Sorryops

Collect and reuse ORIOKS test answers

目前為 2024-04-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name           Sorryops
// @name:ru        Сориупс
// @namespace      https://git.disroot.org/electromagneticcyclone/sorryops
// @version        20240427.1
// @description    Collect and reuse ORIOKS test answers
// @description:ru Скрипт для сбора и переиспользования ответов на тесты ОРИОКС
// @icon           https://orioks.miet.ru/favicon.ico
// @author         electromagneticcyclone & angelbeautifull
// @license        Unlicense
// @supportURL     https://git.disroot.org/electromagneticcyclone/sorryops
// @match          https://orioks.miet.ru/student/student/test*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_registerMenuCommand
// @grant          GM_setClipboard
// @require        https://openuserjs.org/src/libs/sizzle/GM_config.js
// @run-at         document-start
// ==/UserScript==

/* Labels */

var all_labels = {
    en: {
        l: "English",
        settings_title: "Settings",
        script_language: "Language",
        auto_answer: "Auto answer",
        auto_answer_no: "No",
        auto_answer_first: "First",
        auto_answer_random: "Random",
        display_answer: "Display answer near variant",
        stop_timer: "Freeze and hide timer",
        register_keyboard_keys: "Register hotkeys",
        copy_answers: "Copy results to the clipboard",
        append_question_number: "Show question numbers in the final report",
        accumulator_enable: "Accumulate test results in one field",
        auto_continue: "Auto continue (DANGEROUS!!! Will be disabled after an hour. Press `d` to disable)",
        auto_restart: "Auto restart (DANGEROUS!!! Will be disabled after an hour. Press `d` to disable. Make sure you have infinite attempts)",
    },
    ru: {
        l: "Русский",
        settings_title: "Настройки",
        script_language: "Язык",
        auto_answer: "Автовыбор ответа",
        auto_answer_no: "Нет",
        auto_answer_first: "Первый",
        auto_answer_random: "Случайный",
        display_answer: "Отображать ответ рядом с вариантом",
        stop_timer: "Заморозить и скрыть таймер",
        register_keyboard_keys: "Горячие клавиши",
        copy_answers: "Копировать результаты в буфер обмена",
        append_question_number: "Отображать номер вопроса в финальном отчёте",
        accumulator_enable: "Собирать отчёты в одно поле",
        auto_continue: "Автопродолжение (ОПАСНО!!! Отключается через час. Нажмите `d`, чтобы остановить)",
        auto_restart: "Автоперезапуск (ОПАСНО!!! Отключается через час. Нажмите `d`, чтобы остановить. Убедитесь, что количество попыток неограничено)",
    },
};

var labels = all_labels[(() => {
    var lang = GM_getValue('language', "-");
    if (!lang || (lang == "-")) {
        lang = navigator.language || navigator.userLanguage;
    }
    for (var l in all_labels) {
        if (lang.includes(l)) {
            return l;
        }
    }
})()];
if (labels == undefined) {
    labels = all_labels.ru;
}

/* End Labels */

/* Config */

var config = new GM_config({
    id: 'config',
    title: labels.settings_title,
    fields: {
        script_language: {
            label: labels.script_language,
            type: 'select',
            options: [
                '-',
                all_labels.en.l,
                all_labels.ru.l,
            ],
            default: '-',
        },
        auto_answer: {
            label: labels.auto_answer,
            type: 'select',
            options: [
                labels.auto_answer_no,
                labels.auto_answer_first,
                labels.auto_answer_random,
            ],
            default: labels.auto_answer_no,
        },
        display_answer: {
            label: labels.display_answer,
            type: 'checkbox',
            default: true,
        },
        stop_timer: {
            label: labels.stop_timer,
            type: 'checkbox',
            default: true,
        },
        register_keyboard_keys: {
            label: labels.register_keyboard_keys,
            type: 'checkbox',
            default: true,
        },
        copy_answers: {
            label: labels.copy_answers,
            type: 'checkbox',
            default: false,
        },
        append_question_number: {
            label: labels.append_question_number,
            type: 'checkbox',
            default: true,
        },
        accumulator_enable: {
            label: labels.accumulator_enable,
            type: 'checkbox',
            default: false,
        },
        auto_continue: {
            label: labels.auto_continue,
            type: 'checkbox',
            default: false,
        },
        auto_continue_time: {
            type: 'hidden',
            default: 0,
        },
        auto_restart: {
            label: labels.auto_restart,
            type: 'checkbox',
            default: false,
        },
        auto_restart_time: {
            type: 'hidden',
            default: 0,
        },
    },
    events: {
        init: function() {
            GM_setValue('stop_timer', this.get('stop_timer'));
            if (this.get('auto_continue') && (this.get('auto_answer') == "No")) {
                this.set('auto_continue', false);
            }
            if (this.get('accumulator_enable') == false) {
                GM_setValue('accumulated_answers', "");
            }
            switch (this.get('script_language')) {
                case all_labels.en.l:
                    GM_setValue('language', "en");
                    break;
                case all_labels.ru.l:
                    GM_setValue('language', "ru");
                    break;
                default:
                    GM_setValue('language', "-");
                    break;
            }
        },
        save: function(forgotten) {
            this.set('auto_continue_time', Date.now());
            this.set('auto_restart_time', Date.now());
            if (this.isOpen && this.get('auto_continue') && (this.get('auto_answer') == "No")) {
                this.set('auto_continue', false);
                alert("Can't automatically continue without answer.");
            }
            this.init();
        },
    },
});

GM_registerMenuCommand(labels.settings_title, () => {
    config.open();
});

/* End Config */

/* Server */

// Local
function S_setValue(field, value) {
    var v = GM_getValue("server", value);
}

// Local
function S_getValue(field, default_value) {
    var v = GM_getValue("server", default_value);
}

/* End Server */

/* Stop timer */

if (GM_getValue('stop_timer', true)) {
    var i, pbox;
    var pboxes = document.getElementsByTagName('p');
    for (i = 0; i < pboxes.length; i++) {
        pbox = pboxes[i];
        if (pbox.textContent.includes("Осталось:")) {
            pbox.parentNode.remove();
            document.getElementsByTagName('hr')[0].remove();
            var injectFakeTimer = function(window) {
                window.setInterval = (f, t) => {
                    return window.setInterval(f, 10^999);
                };
            }
            var scriptFakeTimer = document.createElement('script');
            scriptFakeTimer.setAttribute("type", "application/javascript");
            scriptFakeTimer.textContent = '(' + injectFakeTimer + ')(window);';
            document.body.appendChild(scriptFakeTimer);
            break;
        }
    }
}

/* End Stop timer */

/* Events */

window.addEventListener('load', main);
window.onkeydown = (e) => {
    if ((e.key == "Enter") && config.get('register_keyboard_keys')) {
        press_continue_btn();
    }
    if (e.key == "d") {
        config.set('auto_continue', false);
        config.set('auto_restart', false);
        config.save();
    }
};

/* End Events */

/* Page properties */

// const success = -1487162948;
var answers = [];
var variant, hash, type;
var testID = (() => {
    var url = document.URL;
    url = url.slice(url.indexOf("idKM=") + 5);
    url = url.slice(0, url.indexOf("&"));
    return url;
})();

/* End properties */

/* Functions */

// https://stackoverflow.com/a/15710692
function hashCode(s) {
    return s.split("").reduce(function(a, b) {
        a = ((a << 5) - a) + b.charCodeAt(0);
        return a & a;
    }, 0);
}

function DB_cleaner() {
    var clear = GM_getValue('clear_tests', new Object());
    var tests = GM_getValue('tests', new Object());
    for (var test in clear) {
        delete tests[test];
    }
    GM_setValue('tests', tests);
    GM_setValue('clear_tests', new Object());
}

function press_continue_btn() {
    var i;
    var buttons = document.getElementsByTagName('button');
    var button = undefined;
    for (i = 0; i < buttons.length; i++) {
        var btn = buttons[i];
        if (btn.textContent.includes("Пройти") || btn.textContent.includes("Продолжить")) {
            button = btn;
            break;
        }
    }
    if (button === undefined) {
        return;
    }
    if (button.textContent.includes("Пройти")) {
        window.location.replace(button.parentNode.href);
    } else if (button.textContent.includes("Продолжить")) {
        button.click();
    }
}

function calculate_variant_hash() {
    variant = document.getElementById('w0').parentNode.textContent;
    variant = variant.slice(variant.indexOf("Вопрос:"));
    hash = hashCode(variant);
}

function update_variant() {
    var i, pbox;
    var chosen_answer = "";
    switch (type) {
        case 'checkbox':
        case 'radio': {
            for (i = 0; i < answers.length; i++) {
                chosen_answer += answers[i].checked ? answers[i].value : "";
            }
            chosen_answer = chosen_answer.split('').sort().join('');
        } break;
        case 'text': {
            for (i = 0; i < answers.length; i++) {
                chosen_answer += "[" + answers[i].value + "]";
            }
        }
    }
    var pboxes = document.getElementsByTagName('p');
    const display_answer = config.get('display_answer');
    for (i = 0; i < pboxes.length; i++) {
        pbox = pboxes[i];
        if (pbox.textContent.includes("Вопрос:")) {
            pbox.innerHTML = "<i>(Вариант <input onfocus='this.select();' id='variant' value='" + hash + (display_answer == true ? (" " + chosen_answer) : "") + "' readonly>)</i><br>Вопрос:";
            break;
        }
    }
    var question_num = undefined;
    for (i = 0; i < pboxes.length; i++) {
        pbox = pboxes[i];
        if (pbox.textContent.includes("Текущий вопрос: ")) {
            question_num = pbox.textContent.slice(variant.indexOf("Текущий вопрос: ") + 16);
            break;
        }
    }
    var tests = GM_getValue('tests', new Object());
    if (tests[testID] === undefined) {
        tests[testID] = new Object();
    }
    tests[testID][hash] = [question_num, chosen_answer];
    GM_setValue('tests', tests);
}

/* End Functions */

/* Handlers */

function test_form_handler() {
    var i;
    var boxes = [];
    var objects = new Object();
    var form = document.getElementById('testform-answer');
    var manual_form = document.getElementById('testform-answer-0');
    if (form != null) {
        boxes = form.getElementsByTagName('input');
    } else if (manual_form != null) {
        i = 1;
        while (manual_form != null) {
            boxes.push(manual_form);
            manual_form = document.getElementById('testform-answer-' + i++);
        }
        console.log(boxes);
    }
    type = boxes[0].type;
    console.log(type);
    switch (type) {
        case 'checkbox':
        case 'radio': {
            for (i = 0; i < boxes.length; i++) {
                var answerHash = hashCode(boxes[i].parentNode.innerText)
                var span = document.createElement('span');
                span.innerHTML =
                  boxes[i].type === 'radio' && boxes[i].value == "1"
                    ? "<b>" + boxes[i].value + ")</b> "
                    : boxes[i].value + ") ";
                boxes[i].parentNode.insertBefore(span, boxes[i]);
                objects[boxes[i].value] = boxes[i];
            }
            const sorted_objects = Object.keys(objects).sort().reduce(
                (obj, key) => {
                    obj[key] = objects[key];
                    return obj;
                }, {}
            );
            for (var key in sorted_objects) {
                sorted_objects[key].parentNode.remove();
                form.appendChild(sorted_objects[key].parentNode);
                answers.push(sorted_objects[key]);
            }
            calculate_variant_hash();
            var correct_answer = S_getValue(testID + "." + hash + ".correct", undefined);
            var incorrect_answers = S_getValue(testID + "." + hash + ".incorrect", undefined);
            const auto_answer = config.get('auto_answer');
            if (auto_answer == labels.auto_answer_random) {
                if (answers[0].type === 'radio') {
                    var chosen_answer = Math.floor(Math.random() * answers.length);
                    answers[chosen_answer].click();
                } else {
                    var pick = Math.floor(Math.random() * (Math.pow(2, answers.length) - 1)) + 1;
                    for (i = 0; i < answers.length; i++) {
                        if(pick & Math.pow(2, i)) {
                            answers[i].click();
                        }
                    }
                }
            } else if (auto_answer == labels.auto_answer_first) {
                answers[0].click();
            }
        } break;
        case 'text': {
            answers = boxes;
            calculate_variant_hash();
        } break;
    }
    update_variant();
    for (i = 0; i < answers.length; i++) {
        answers[i].addEventListener('change', update_variant);
    }
}

function result_page_handler() {
    var i;
    var correct = variant.slice(variant.indexOf("Число верных ответов: ") + 22);
    correct = correct.slice(0, correct.indexOf("\n"));
    var test = GM_getValue('tests', new Object())[testID];
    if (test === undefined) {
        return;
    }
    var printer = "";
    var sorted_test = [];
    for (var hash in test) {
        sorted_test.push([hash].concat(test[hash]));
    }
    sorted_test.sort((a, b) => {return a[1] - b[1]});
    console.log(sorted_test);
    for (i = 0; i < sorted_test.length; i++) {
        printer += (config.get('append_question_number') ? (sorted_test[i][1] + ") ") : "") + sorted_test[i][0] + " " + sorted_test[i][2] + "\n";
    }
    printer += correct;
    if (config.get('copy_answers')) {
        GM_setClipboard(printer);
    }
    if (config.get('accumulator_enable')) {
        var acc = GM_getValue('accumulated_answers', "");
        if (acc != "") {
            acc += "\n\n";
        }
        var prefix = testID;
        if (prefix != "") {
            acc += prefix + "\n";
        }
        acc += printer;
        GM_setValue('accumulated_answers', acc);
        printer = acc;
    }
    printer = "<textarea readonly style='resize:none; width:fit-content; height:fit-content' rows='" + String(Object.keys(test).length + 1) + "' cols='50' onfocus='this.select();' id='answers'>" + printer + "</textarea>";
    var pboxes = document.getElementsByTagName('p');
    for (i = 0; i < pboxes.length; i++) {
        var pbox = pboxes[i];
        if (pbox.textContent.includes("Попытка ")) {
            pbox.outerHTML += printer;
            break;
        }
    }
    var clear = GM_getValue('clear_tests', new Object());
    clear[testID] = true;
    GM_setValue('clear_tests', clear);
}

/* End Handlers */

function main() {
    var old_time, cur_time;
    variant = document.getElementById('w0').parentNode.textContent;
    if (variant.includes("Вопрос:")) {
        DB_cleaner();
        test_form_handler();
        if (config.get('auto_continue')) {
            old_time = config.get('auto_continue_time');
            cur_time = Date.now();
            if (cur_time - old_time > 60 * 60 * 1000) {
                config.set('auto_continue', false);
            } else {
                press_continue_btn();
            }
        }
    } else if (variant.includes("Результат прохождения теста:")) {
        result_page_handler();
        if (config.get('auto_restart')) {
            old_time = config.get('auto_restart_time');
            cur_time = Date.now();
            if (cur_time - old_time > 60 * 60 * 1000) {
                config.set('auto_restart', false);
            } else {
                press_continue_btn();
            }
        }
    }
}