Naurok Bypass v2

Fetches answers to *all* Naurok quizes

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Naurok Bypass v2
// @author      griffi-gh
// @namespace   griffi-gh
// @description Fetches answers to *all* Naurok quizes
// @version     8.1
// @license     MIT
// @match       *://naurok.com.ua/test/*.html
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_getResourceURL
// @run-at      document-start
// @inject-into content
// @sandbox     DOM
// @connect     naurok.com.ua
// @icon        https://play-lh.googleusercontent.com/scIkpmsUJTfDbV39X0rb-AvxbgxOrpa9zIGJQqDHP1VbuBTmortXomSSWVZnpErwyA=w480-h960
// @homepageURL https://greasyfork.org/uk/scripts/461662-naurok-bypass-v2
// @resource    LOADING_IMAGE         https://media.tenor.com/MlCeUwzn2nEAAAAM/troll-lol.gif
// @resource    TEST_IMAGE_URL        https://cdn-icons-png.flaticon.com/512/190/190411.png
// @resource    TEST_UPDATE_IMAGE_URL https://cdn-icons-png.flaticon.com/512/1601/1601884.png
// @resource    ERROR_IMAGE_URL       https://media.tenor.com/hA1b1zIqnHkAAAAd/among-us.gif
// ==/UserScript==

"use strict"; {

console.log("Naurok Bypass v2");

// Check if already loaded
if (window._BNAUROK2 != null) {
  console.error("already loaded");
  return;
} else {
  window._BNAUROK2 = true;
}

//Required to work around the false session issue
//Used only if a direct request fails
const SESSION_PROXY = "https://eobzz8g6oxzrky0.m.pipedream.net/";

let ERROR_IMAGE_URL, LOADING_IMAGE, TEST_IMAGE_URL, TEST_UPDATE_IMAGE_URL;
if (window.GM_getResourceURL) {
  //Use GM resources if possible
  console.log("Using GM resources");
  LOADING_IMAGE         = GM_getResourceURL("LOADING_IMAGE",         true);
  TEST_IMAGE_URL        = GM_getResourceURL("TEST_IMAGE_URL",        true);
  TEST_UPDATE_IMAGE_URL = GM_getResourceURL("TEST_UPDATE_IMAGE_URL", true);
  ERROR_IMAGE_URL = GM_getResourceURL("ERROR_IMAGE_URL", true);
} else {
  console.log("Using URL resources");
  LOADING_IMAGE = "https://media.tenor.com/MlCeUwzn2nEAAAAM/troll-lol.gif";
  TEST_IMAGE_URL = "https://cdn-icons-png.flaticon.com/512/190/190411.png";
  TEST_UPDATE_IMAGE_URL = "https://cdn-icons-png.flaticon.com/512/1601/1601884.png";
  ERROR_IMAGE_URL = "https://media.tenor.com/hA1b1zIqnHkAAAAd/among-us.gif";
}

const ls_key = `cached-${document.location.pathname.replaceAll("/", "-").slice(1, -5).toLowerCase()}`;

async function loadStuff() {
  //Clean up
  pre_display()

  //Get the hostname
  const hostname = window.location.hostname;
  console.log("Host:", hostname);

  //Get test path
  const base = document.location.href.slice(0, -5);
  console.log("Base URL:", base);

  //Load the homework creation page
  const set_text = await fetch(base + "/set").then(x => x.text());
  const set_document = document.createElement("html");
  set_document.innerHTML = set_text;
  console.log("Set document:", set_document);

  //Get form data and modify it
  const set_form = set_document.querySelector("#w0");
  const set_form_data = new FormData(set_form);
  set_form_data.set("Homework[deadline_day]", "9999-01-01");
  set_form_data.set("Homework[show_answer]", "1");
  console.log("Set form data:", set_form_data);

  //Start homework
  const homework_res = await fetch(set_form.action, {
    redirect: 'follow',
    method: 'POST',
    credentials: 'include',
    body: set_form_data,
  });

  //Get homework url and id
  const homework_url = homework_res.url;
  const homework_id = homework_url.split("/").at(-1);
  console.log("Homework url:", homework_url);

  //Get homework document object
  const homework_text = await homework_res.text();
  const homework_document = document.createElement("html");
  homework_document.innerHTML = homework_text;
  console.log("Homework document:", homework_document);

  //Get the CSRF token and create FormData from it
  const homework_csrf_param = homework_document.querySelector('meta[name="csrf-param"]').content;
  const homework_csrf_token = homework_document.querySelector('meta[name="csrf-token"]').content;
  const homework_csrf_form_data = new FormData();
  homework_csrf_form_data.set(homework_csrf_param, homework_csrf_token);
  console.log("CSRF param/token:", homework_csrf_param, homework_csrf_token);

  //Get join code
  const homework_code = homework_document.querySelector(".homework-code").textContent;
  console.log("Homework join code:", homework_code);

  //Load the join page
  const join_text = await fetch(`https://${hostname}/test/join?gamecode=${homework_code}`).then(res => res.text());
  const join_document = document.createElement('html');
  join_document.innerHTML = join_text;

  //Get form data and fill in the username
  const join_form = join_document.querySelector("#participate-form-code");
  const join_form_data = new FormData(join_form);
  const username = "[object Object]";
  join_form_data.set("JoinForm[name]", username); //troll naurok developers while we're at it lol

  //Start homework session
  const join_res = await fetch(join_form.action, {
    redirect: 'follow',
    method: 'POST',
    credentials: 'include',
    body: new URLSearchParams(join_form_data).toString(),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });
  console.log("Joined with username", username)

  //Get test session address
  const test_session_url = join_res.url;
  console.log("Test session URL:", test_session_url);

  //Get the test session document
  const session_text = await fetch(test_session_url).then(res => res.text());
  const session_document = document.createElement("html");
  session_document.innerHTML = session_text;

  //Get session id
  const testik_elem = session_document.querySelector('[ng-app="testik"]');
  const ng_init = testik_elem.getAttribute("ng-init");
  const ng_init_numbers = ng_init.match(/[0-9]+/g);
  const session_id = ng_init_numbers[1] || 0;
  console.log("Session id", session_id);

  //Get session info
  let session_info = await fetch(`https://${hostname}/api2/test/sessions/${session_id}`, {
    credentials: "include",
    headers: {
      'Accept': 'application/json, text/plain, */*',
      'Content-Type': 'application/json'
    },
    redirect: 'follow',
  }).then(x => x.json());
  console.log("Session info: ", session_info);

  //Work around weird issue
  if (session_info == false) {
    console.log("Oops, need to proxy the request");
    session_info = await fetch(SESSION_PROXY, {
      method: "POST",
      headers: {
        'Accept': 'application/json, text/plain, */*',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        session: session_id
      })
    }).then(x => x.json());
    console.log("Session info (proxied):", session_info);
  }

  //Find the latest question
  const {latest_question, questions} = session_info;
  const question = latest_question ? questions.find(question => question.id == latest_question) : questions[0];
  console.log("Question:", question);

  //Get the answer id
  const answer_id = question.options[0].id.toString();
  console.log("Answer ID:", answer_id);

  //Answer the question
  await fetch(`https://${hostname}/api2/test/responses/answer`, {
    method: "PUT",
    credentials: "include",
    headers: {
      'Accept': 'application/json, text/plain, */*',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      "session_id": session_id,
      "answer": [answer_id],
      "question_id": question.id,
      "show_answer": 1,
      "type": "quiz",
      "point": question.point.toString(),
      "homeworkType": question.type,
      "homework": true
    }),
  });
  console.log("Responded to the question");

  //End the session
  const end_sess_data = await fetch(`https://${hostname}/api2/test/sessions/end/${session_id}`, {
    "method": "PUT",
    "credentials": "include",
  }).then(res => res.json());
  const end_sess_uuid = end_sess_data.session.uuid;
  console.log("Session UUID:", end_sess_uuid);

  //Fetch the end page
  const test_end_text = await fetch(`https://${hostname}/test/complete/${end_sess_uuid}`, {
    redirect: 'follow'
  }).then(res => res.text());

  //Get the text end document
  const test_end_document = document.createElement("html");
  test_end_document.innerHTML = test_end_text;

  //Get the answers
  const answers = test_end_document.querySelector(".homework-stats");
  console.log("Answer element:", answers);

  //Display answers
  display_answers(answers);
  console.log("Answers displayed");

  //Stop homework
  await fetch(`https://${hostname}/test/homework/${homework_id}/stop`, {
    method: 'POST',
    credentials: 'include',
    body: homework_csrf_form_data,
  });
  console.log("Homework stopped");

  //Delete homework
  await fetch(`https://${hostname}/test/homework/${homework_id}/delete`, {
    method: 'POST',
    credentials: 'include',
    body: homework_csrf_form_data,
  });
  console.log("Homework deleted");

  console.log("DONE");

  return answers
};

function pre_display() {
  //Delete previously displayed
  Array.from(document.querySelectorAll(".question-view-item")).forEach(item => item.remove());

  //Clear the regular questions
  Array.from(document.querySelectorAll(".answer-sheet")).forEach(item => item.remove());
}

function display_answers(answers) {
  pre_display()

  //Add classes
  answers.classList.add("row");
  answers.classList.add("answer-sheet")

  //HACK: Remove "- your answer" text
  answers.innerHTML = answers.innerHTML.replaceAll("<em>— ваша відповідь</em>", "");

  //Add answers to the page
  const afer_element = document.querySelector(".block-head");
  afer_element.parentNode.insertBefore(answers, afer_element.nextSibling);
};

async function loadStuffWriteCache() {
  const answers = await loadStuff();
  const elem = document.createElement("div");
  display_answers(elem);
  elem.appendChild(answers.cloneNode(true));
  (window.GM_setValue ?? localStorage.setItem.bind(localStorage))(ls_key, elem.innerHTML);
}

//Preload images
const images = []
{
  window._cow_taxes = images; //keep the reference alive
  images["loading"] = new Image();
  images["loading"].src = LOADING_IMAGE;
  images["test"] = new Image();
  images["test"].src = TEST_IMAGE_URL;
  images["update"] = new Image();
  images["update"].src = TEST_UPDATE_IMAGE_URL;
  images["error"] = new Image();
  images["error"].src = ERROR_IMAGE_URL;
}

function loadErrorHandler(err) {
  console.error(err);
  const btn = document.querySelector(".clicky-click");
  btn.querySelector("img").replaceWith(images.error);
  const label = btn.querySelector("span")
  label.textContent = "Помилка\n"+err.toString()+"\n";
  label.innerHTML = label.innerHTML.replaceAll("\n", "<br>");
  label.innerHTML += `
    FIX 1 - Натисни кнопку ще раз<br>
    FIX 2 - <a href='https://naurok.com.ua/test/join?gamecode=0' target="_blank">Відкрий і закрий цю сторінку</a><br>
    FIX 3 - <a href='https://naurok.com.ua/login' target="_blank">Увійди в аккаунт Наурок</a><br>
    FIX 4 - Спробуй ще раз через 5 хвилин
  `;
}

async function loadStuffAndWriteCacheWithErrorHandler() {
  try {
    await loadStuffWriteCache();
    return true;
  } catch(err) {
    loadErrorHandler(err);
    return false;
  }
}

//Load cached answers
let is_cached = false;
let cached_element = null;
if ((window.GM_getValue ?? localStorage.getItem.bind(localStorage))(ls_key, null)) {
  console.log("Cached found:", ls_key);
  const elem = document.createElement("div");
  try {
    elem.innerHTML = (GM_getValue ?? localStorage.getItem.bind(localStorage))(ls_key, null);
    cached_element = elem.firstChild;
    is_cached = true;
    (cached_element.querySelector(".chicken-beef")?.classList.add("answer-sheet")) && console.warn("Cache contains chicken beef");
  } catch (e) {
    console.error("Cache invalid:", e);
    is_cached = false;
    cached_element = null;
  }
}

const MAIN = async () => {
  //Display cached stuff
  if (is_cached) {
    try {
      display_answers(cached_element);
      console.log("Cached answer displayed!")
    } catch(e) {
      console.error("Cache invalid:", e);
      is_cached = false;
      cached_element = null;
    }
  }

  //Add CSS
  {
    const style = `

      .answer-sheet {
        padding: 1.33rem;
      }

      /* style indicator colors*/
      .answer-sheet .homework-stat-option-value.incorect :is(.quiz,.multiquiz) {
        background: #cccccc !important;
        color: black !important;
      }
      .answer-sheet .homework-stat-option-value.correct :is(.quiz,.multiquiz) {
        background: #23c552 !important;
        color: black !important;
      }

      /* fix indicator offset */
      .answer-sheet .homework-stat-option-value :is(.quiz,.multiquiz) {
        top: .5rem !important;
      }

      /* Green bg on correct*/
      .answer-sheet .homework-stat-option-value.correct {
        background: linear-gradient(to bottom, rgba(0,0,0,0), #e3f7e9 15%, #e3f7e9 85%, rgba(0,0,0,0)) !important;
        border-radius: .25rem !important;
      }

      /* fix uneven p padding*/
      .answer-sheet .homework-stat-option-value p {
        margin: 0.5rem 0.25rem !important;
      }

      /* hide q ranking */
      .answer-sheet .question-label {
        display: none !important;
      }

      /* hide weird orange overlays */
      .answer-sheet .ql-cursor {
        display: none !important;
      }

      /* hide correctness indicators*/
      .answer-sheet .content-block.success,
      .answer-sheet .content-block.skipped,
      .answer-sheet .content-block.failed,
      .answer-sheet .content-block.partial {
        border-left: none !important;
      }

      /* style the main button */
      .clicky-click {
        display: flex;
        width: 100%;
        border-width: 0;
        font-family: inherit;
        font-size: inherit;
        font-style: inherit;
        font-weight: inherit;
        line-height: inherit;
        margin-bottom: 0 !important;
      }
      #cb_wrapper {
        display: blck;
        text-align: center;
      }
      #auto_load_cb {
        margin-right: .25rem;
      }
      #auto_load_cb ~ label {
        font-weight: unset;
      }

      /* Use flex to style our button */
      .test-action-button.clicky-click {
        display: flex !important;
        flex-direction: column !important;
        height: unset !important;
        gap: 10px !important;
      }
      .test-action-button.clicky-click * {
        position: unset !important;
      }

      /* This applies to ALL buttons (makes them a bit fancier because why not) */
      .test-action-button {
        transition: all .25s !important;
        border: 1px solid rgba(0,0,0,.1) !important;
        border-radius: 10px !important;
      }
      .test-action-button:hover {
        background: #f0f0f0 !important;
      }
      .test-action-button:hover > * {
        filter: drop-shadow(0px 0px 4px #dddddd);
      }
      .test-action-button > img {
        transition: transform .25s !important;
      }
      .test-action-button:not(:disabled):hover > img {
        transform: scale(0.9) rotate(-3deg);
      }

      /* todo: style auto load checkbox */
      #auto_load_cb {}

      /* fix scrolling */
      body {
        overflow: auto !important;
      }

      /* Remove ads */
      /* TODO: fix! hides answers too */
      /*.col-sm-8.col-md-9 {
        display: none !important;
      }*/

      /* Fix em coloring */
      .homework-stat-option-value em {
        color: inherit !important;
      }
    `;
    const style_elem = document.createElement("style");
    style_elem.textContent = style;
    document.head.appendChild(style_elem);
  }

  //Create answers button
  {
    const button = document.createElement("button");
    button.type = "button";
    button.classList.add("test-action-button");
    button.classList.add("clicky-click");
    button.appendChild(images.test);
    const text_elem = document.createElement("span");
    text_elem.textContent = "Завантажити відповіді";
    button.appendChild(text_elem);
    if (is_cached) {
      button.querySelector('img').replaceWith(images.update);
      text_elem.textContent = "Оновити відповіді";
    }
    button.addEventListener("click", async () => {
      button.querySelector('img').replaceWith(images.loading);
      text_elem.textContent = "Завантаження...";
      button.disabled = true;
      if (await loadStuffAndWriteCacheWithErrorHandler()) {
        button.querySelector('img').replaceWith(images.update);
        text_elem.textContent = "Оновити відповіді";
      }
      button.disabled = false;
    });
    const buttons = document.querySelector(".single-test-actions");
    buttons.prepend(button);
  }

  //Create auto load toggle
  {
    //Create checkbox
    const auto_load_cb = document.createElement("input");
    auto_load_cb.id = "auto_load_cb";
    auto_load_cb.type = "checkbox";
    const save_state = () => {
      (window.GM_setValue || localStorage.setItem.bind(localStorage))("auto-load", auto_load_cb.checked ? "1" : "0");
    }
    auto_load_cb.checked = ((window.GM_getValue || localStorage.getItem.bind(localStorage))("auto-load") || "0") == "1";
    save_state();
    auto_load_cb.addEventListener("change", save_state);
    if (!is_cached && auto_load_cb.checked) {
      document.querySelector(".clicky-click").click();
    }
    //Add it
    const cb_wrapper = document.createElement("div");
    cb_wrapper.id = "cb_wrapper";
    const cb_label = document.createElement("label");
    cb_label.textContent = "Автоматично завантажувати відповіді";
    cb_label.setAttribute("for", auto_load_cb.id);
    cb_wrapper.appendChild(auto_load_cb);
    cb_wrapper.appendChild(cb_label);
    const afer_element = document.querySelector(".clicky-click");
    afer_element.parentNode.insertBefore(cb_wrapper, afer_element.nextSibling);
  }
};

// Run if loaded late
if ((document?.readyState == "interactive") || (document?.readyState == "complete")) {
  MAIN();
} else {
  document.addEventListener("DOMContentLoaded", MAIN);
}

}