Naurok Bypass v2

Fetches answers to *all* Naurok quizes

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
}

}