// ==UserScript==
// @name Sorryops
// @name:ru Сориупс
// @namespace https://git.disroot.org/electromagneticcyclone/sorryops
// @version 20240429.2
// @description Collect and reuse ORIOKS test answers
// @description:ru Скрипт для сбора и переиспользования ответов на тесты ОРИОКС
// @icon https://orioks.miet.ru/favicon.ico
// @author electromagneticcyclone & angelbeautifull
// @license GPL-3.0-or-later
// @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
// @grant GM_xmlhttpRequest
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @connect sorryops.ru
// @run-at document-start
// ==/UserScript==
/* Charset */
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/* End Charset */
/* Labels */
const all_labels = {
en: {
l: "English",
settings_title: "Settings",
script_language: "Language",
show_user_id: "Show user ID",
user_id: "User ID (keep private)",
server: "Sync answers with server (leave blank to disable)",
auto_answer: "Auto answer",
auto_answer_no: "No",
auto_answer_first: "First",
auto_answer_random: "Random",
display_values: "Answers variant",
display_values_ori: "ORIOKS",
display_values_sorry: "Sorry",
display_values_both: "Both",
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: "Язык",
show_user_id: "Показать индетификатор пользователя",
user_id: "Индетификатор (держать в секрете)",
server: "Синхронизировать ответы с сервером (оставить пустым для отключения)",
auto_answer: "Автовыбор ответа",
auto_answer_no: "Нет",
auto_answer_first: "Первый",
auto_answer_random: "Случайный",
display_values: "Вариант отображения ответов",
display_values_ori: "ОРИОКС",
display_values_sorry: "Сори",
display_values_both: "Оба",
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: '-',
},
show_user_id: {
label: labels.show_user_id,
type: 'checkbox',
default: false,
},
user_id: {
label: labels.user_id + (GM_getValue("show_user_id", false) ? "" : "<input readonly value='******'>"),
type: GM_getValue("show_user_id", false) ? 'text' : 'hidden',
save: false,
default: '',
},
server: {
label: labels.server,
type: 'text',
default: '',
},
valid_user_id: {
type: 'hidden',
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_values: {
label: labels.display_values,
type: 'select',
options: [
labels.display_values_ori,
labels.display_values_sorry,
labels.display_values_both,
],
default: labels.display_values_ori,
},
display_answer: {
label: labels.display_answer,
type: 'checkbox',
default: true,
},
stop_timer: {
label: labels.stop_timer,
type: 'checkbox',
default: false,
},
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() {
var valid_user_id = this.get('valid_user_id');
if (!validate_user_id(valid_user_id)) {
valid_user_id = generate_user_id();
}
this.set('user_id', valid_user_id);
this.set('valid_user_id', valid_user_id);
GM_setValue('show_user_id', this.get('show_user_id'));
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.");
}
if (!validate_user_id(forgotten['user_id'])) {
this.set('user_id', this.get('valid_user_id'))
alert('User ID is invalid!');
} else {
this.set('valid_user_id', forgotten['user_id'])
}
this.init();
},
},
});
GM_registerMenuCommand(labels.settings_title, () => {
config.open();
});
/* End Config */
/* Server */
function send_to_server(results) {
var server = config.get('server');
if (server != '') {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://' + server,
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify(results),
});
}
}
function fetch_from_server(path, func) {
var server = config.get('server');
if (server != '') {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://' + server + '/' + path,
onload: function (response) {
var text = response.responseText;
if (!text.includes("{")) {
func({});
} else {
func(JSON.parse(text));
}
},
onerror: function (e) {
func({});
},
onabort: function (e) {
func({});
},
ontimeout: function (e) {
func({});
}
});
} else {
func({});
}
}
/* 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://github.com/ajayyy/maze-utils/blob/036086403f675b8fea0e22065f26ba534e351562/src/setup.ts
function generate_user_id(length = 36) {
var i;
var result = "";
const cryptoFuncs = typeof window === "undefined" ? crypto : window.crypto;
if (cryptoFuncs && cryptoFuncs.getRandomValues) {
const values = new Uint32Array(length);
cryptoFuncs.getRandomValues(values);
for (i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
} else {
for (i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
}
return result;
}
function validate_user_id(uid, length = 36) {
var i;
if (uid.length != length) {
return false;
}
for (i = 0; i < length; i++) {
if (!charset.includes(uid[i])) {
return false;
}
}
return true;
}
// https://stackoverflow.com/a/15710692
function hashCode(s, return_num = false) {
var result = "";
var h = s.split("").reduce(function(a, b) {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0);
if (return_num) {
return h;
}
while (h != 0) {
result += charset[((h % charset.length) + charset.length) % charset.length];
h = Math.floor(Math.abs(h) / charset.length) * (h / Math.abs(h));
}
return result;
}
function set_to_clear(id, exec_if_not_cleared) {
var clear = GM_getValue('clear_tests', new Object());
if (!clear[id]) {
exec_if_not_cleared();
}
clear[id] = true;
GM_setValue('clear_tests', clear);
}
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].sorry_value : "";
}
chosen_answer = chosen_answer.split('').sort().join('');
if (type == 'checkbox') {
chosen_answer = "{" + chosen_answer + "}";
}
} 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).trim();
break;
}
}
if (hash !== undefined) {
var tests = GM_getValue('tests', new Object());
if (tests[testID] === undefined) {
tests[testID] = new Object();
}
tests[testID][hash] = [question_num, chosen_answer, answers.length];
GM_setValue('tests', tests);
}
}
/* End Functions */
/* Handlers */
function test_form_handler(server_data) {
var i, key, correct, incorrect, answer, sorry_val;
var boxes = [];
var sorted_objects;
var objects_hash = new Object();
var objects_value = 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++);
}
}
type = boxes[0].type;
switch (type) {
case 'checkbox':
case 'radio': {
for (i = 0; i < boxes.length; i++) {
boxes[i].hash = hashCode(boxes[i].parentNode.innerText, true);
objects_hash[boxes[i].hash] = boxes[i];
objects_value[boxes[i].value] = boxes[i];
}
const sorted_objects_hash = Object.keys(objects_hash).sort().reduce(
(obj, key) => {
obj[key] = objects_hash[key];
return obj;
}, {}
);
const sorted_objects_value = Object.keys(objects_value).sort().reduce(
(obj, key) => {
obj[key] = objects_value[key];
return obj;
}, {}
);
i = 0;
sorted_objects = sorted_objects_hash;
for (key in sorted_objects) {
sorted_objects[key].parentNode.remove();
form.appendChild(sorted_objects[key].parentNode);
}
calculate_variant_hash();
if (server_data.hasOwnProperty('correct')) {
correct = server_data.correct;
if (correct === undefined) {
correct = undefined;
} else if (correct.hasOwnProperty(hash)) {
correct = correct[hash];
} else {
correct = undefined;
}
} else {
correct = undefined;
}
if (server_data.hasOwnProperty('incorrect')) {
incorrect = server_data.incorrect;
if (incorrect === undefined) {
incorrect = [];
} else if (incorrect.hasOwnProperty(hash)) {
incorrect = incorrect[hash];
} else {
incorrect = [];
}
} else {
incorrect = [];
}
for (key in sorted_objects) {
sorted_objects[key].sorry_value = charset[i++];
var span = document.createElement('span');
var disp_val;
switch (config.get('display_values')) {
case labels.display_values_ori:
disp_val = sorted_objects[key].value;
break;
case labels.display_values_sorry:
disp_val = sorted_objects[key].sorry_value;
break;
case labels.display_values_both:
disp_val = sorted_objects[key].value + ":" + sorted_objects[key].sorry_value;
break;
}
span.innerHTML = disp_val + ") ";
sorted_objects[key].parentNode.insertBefore(span, sorted_objects[key]);
answers.push(sorted_objects[key]);
}
if (config.get('display_values') == labels.display_values_ori) {
for (key in sorted_objects_value) {
sorted_objects_value[key].parentNode.remove();
form.appendChild(sorted_objects_value[key].parentNode);
}
}
const auto_answer = config.get('auto_answer');
if (correct != undefined) {
for (answer in answers) {
if (answers[answer].sorry_value == correct) {
var correct_element = answers[answer].parentNode;
sorry_val = answers[answer].sorry_value;
correct_element.innerHTML = "<div style='color: green;'>" + correct_element.innerHTML + "</div>";
answers[answer] = correct_element.getElementsByTagName('input')[0];
answers[answer].sorry_value = sorry_val;
answers[answer].click();
break;
}
}
} else if (auto_answer == labels.auto_answer_random) {
if (answers[0].type === 'radio') {
var possible_answers = [];
for (answer in answers) {
if (incorrect.includes(answers[answer].sorry_value) == false) {
possible_answers.push(answer);
} else {
var incorrect_element = answers[answer].parentNode;
sorry_val = answers[answer].sorry_value;
incorrect_element.innerHTML = "<div style='color: red;'>" + incorrect_element.innerHTML + "</div>";
answers[answer] = incorrect_element.getElementsByTagName('input')[0];
answers[answer].sorry_value = sorry_val;
answers[answer] = incorrect_element.getElementsByTagName('input')[0];
}
}
var chosen_answer;
chosen_answer = Math.floor(Math.random() * possible_answers.length);
answers[possible_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) {
Object.values(sorted_objects_value)[0].click();
}
} break;
case 'text': {
answers = boxes;
calculate_variant_hash();
if (server_data.hasOwnProperty('correct')) {
correct = server_data.correct[hash];
}
if (server_data.hasOwnProperty('correct')) {
incorrect = server_data.incorrect[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);
var all = variant.slice(variant.indexOf("Число неверных ответов: ") + 24);
correct = correct.slice(0, correct.indexOf("\n")).trim();
all = all.slice(0, all.indexOf("\n")).trim();
all = (parseInt(all) + parseInt(correct)).toString();
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]});
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;
}
}
set_to_clear(testID, () => {
send_to_server({
type: "test_results",
uid: config.get('user_id'),
id: testID,
answers: sorted_test,
correct: correct,
all: all,
});
});
}
/* End Handlers */
function main() {
var old_time, cur_time;
variant = document.getElementById('w0').parentNode.textContent;
if (variant.includes("Вопрос:")) {
fetch_from_server(testID, (server_response) => {
DB_cleaner();
test_form_handler(server_response);
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();
}
}
}
}