Greasy Fork is available in English.

巴哈動漫電玩通題庫與解答系統

巴哈動漫電玩通題庫與解答系統,蒐集題庫中~

// ==UserScript==
// @name         巴哈動漫電玩通題庫與解答系統
// @namespace    https://home.gamer.com.tw/moontai0724
// @version      5.1.0
// @description  巴哈動漫電玩通題庫與解答系統,蒐集題庫中~
// @author       moontai0724
// @match        https://forum.gamer.com.tw/B.php*
// @supportURL   https://home.gamer.com.tw/creationDetail.php?sn=3924920
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @connect      script.google.com
// @connect      script.googleusercontent.com
// @resource     quizrp https://raw.githubusercontent.com/moontai0724/bahamut-quiz-script/master/right-box.html
// @license      MIT
// ==/UserScript==
/** @class */
class Database {
  /** @type {String} */
  static URL = "https://script.google.com/macros/s/AKfycbxYKwsjq6jB2Oo0xwz4bmkd3-5hdguopA6VJ5KD/exec";

  constructor() {
    throw new Error("New a Database instance is not needed.");
  }

  /**
   * Check is quiz exists in database
   * @param {Quiz} quiz 
   * @returns {Boolean}
   */
  static async exists(quiz) {
    let exists = await this.fetch("checkExisting", quiz.sn).catch(() => false);
    return exists ? true : false;
  }

  /**
   * Find answer of quiz in database
   * @param {Quiz} quiz 
   */
  static async answerOf(quiz) {
    let data = await this.fetch("answer", quiz.sn);
    return Promise.resolve(data);
  }

  /**
   * Find hint of quiz in database
   * @param {Quiz} quiz 
   */
  static async hintOf(quiz) {
    let data = await this.fetch("hint", quiz.sn);
    return Promise.resolve(data);
  }

  /**
   * Fetch (get) data from database
   * @param {String} type type of action
   * @param {Number} sn serial number of quiz
   */
  static async fetch(type, sn) {
    const data = new URLSearchParams({ type: type, sn: sn });
    const response = await fetch(`${Database.URL}?${data.toString()}`)
      .then(response => response.json());
    if (response.success)
      return Promise.resolve(response.data);

    return Promise.reject(response);
  }

  /**
   * Submit (post) data to database
   * @param {Quiz} quiz quiz infos
   * @param {Number} answer answer of quiz
   * @param {Boolean} correctness correctness of answer
   */
  static async submit(quiz, answer, correctness) {
    const data = {
      "version": GM_info.script.version,
      "sn": quiz.sn,
      "question": quiz.question,
      "options": quiz.options,
      "bsn": quiz.bsn,
      "author": quiz.author,
      "this_answered": answer,
      "correctness": correctness,
    };

    const response = await fetch(Database.URL, {
      method: "POST",
      cache: "no-cache",
      headers: {
        "content-type": "text/plain;charset=utf-8",
      },
      body: JSON.stringify(data),
    });
    return await response.json();
  }
}
/** @class */
class Quiz {
  /** @type {Number} */
  sn;
  /** @type {String} */
  question;
  /** @type {Array<String>} */
  options;
  /** @type {String} */
  author;
  /** @type {Number} */
  answer;
  /** @type {Number} */
  bsn;
  /** @type {Boolean} */
  answered = false;

  constructor() {
    let qabox = document.querySelector(".BH-qabox1");
    this.sn = qabox.getAttribute("data-quiz-sn");
    this.question = encodeURIComponent(qabox.innerText.split("\n").shift());
    this.options = Array.from(qabox.querySelectorAll("li a")).map(element => encodeURIComponent(element.innerText));
    this.author = new URL("https://" + qabox.querySelector("span>a").getAttribute("href")).pathname.split("/").pop();
    this.bsn = new URL(location.href).searchParams.get("bsn");
  }

  /**
   * try and submit answer to database
   * @param {User} user user to get CSRFToken
   * @param {View} view 
   * @param {Number|null} answer if provided, will test answer correctness
   */
  async submitAnswer(user, view, answer = undefined) {
    let token = await user.getCSRFToken();
    if (!answer || !(await this.attemptAnswer(answer, token, view)))
      answer = await this.getAnswer(token);

    let response = await Database.submit(this, answer, true);
    view.setSubmitResult(response);
  }

  /**
   * Try answer correctness and return correct answer
   * @param {String} token CSRFToken
   * @param {Number} tryAnswer answer index to try submit
   */
  async getAnswer(token, tryAnswer = 1) {
    if (tryAnswer > 4) return Promise.reject();
    const correctness = await this.attemptAnswer(tryAnswer, token);
    if (correctness) {
      this.answer = tryAnswer;
      return tryAnswer;
    }
    return await this.getAnswer(token, ++tryAnswer);
  }

  /**
   * Submit quiz answer to Bahamut
   * @param {Number} answer answer number, valid from 1 to 4
   * @param {String} token CSRFToken
   * @param {View} view if provided, will update response into view
   */
  async attemptAnswer(answer, token, view = undefined) {
    const data = new URLSearchParams({ sn: this.sn, o: answer, token: token });
    const response = await fetch(`/ajax/quiz_answer.php?${data.toString()}`).then(response => response.text());
    if (view)
      view.setResponse(response);
    return response.includes("答對");
  }

  toString() {
    let data = { sn: this.sn, question: this.question, options: this.options, author: this.author, answer: this.answer, bsn: this.bsn };
    return JSON.stringify(data);
  }
}
/** @class */
class User {
  /** @type {String|undefined} */
  id;

  constructor() {
    this.initializeId();
  }

  initializeId() {
    try {
      this.id = BAHAID;
    } catch (error) {
      let cookie = document.cookie.split("; ").filter(cookie => cookie.startsWith("BAHAID")).shift();
      this.id = cookie ? cookie.split("=").pop() : undefined;
    }
  }

  async getCSRFToken() {
    const response = await fetch("/ajax/getCSRFToken.php");
    return await response.text();
  }
}
/** @class */
class View {
  /** @type {HTMLElement} */
  window;
  /** @type {HTMLElement} */
  qabox;

  /**
   * @param {User} user 
   * @param {Quiz} quiz 
   */
  constructor(user, quiz) {
    this.window = document.querySelector("#quizrp");
    this.qabox = document.querySelector(".BH-qabox1");

    this.window.querySelector(".version span").innerHTML = GM_info.script.version;
    this.initOptions(user, quiz);
    this.initAutoAnswer(user, quiz);
    this.window.querySelector(".hint button").addEventListener("click", event => this.showHint(quiz));
    this.window.querySelector(".original button").addEventListener("click", event => this.showQuiz(quiz));
  }

  /**
   * @param {User} user 
   * @param {Quiz} quiz 
   */
  initAutoAnswer(user, quiz) {
    if (GM_getValue("auto-answer", false)) {
      this.window.querySelector(".auto span").classList.add("enable");
      setTimeout(() => quiz.submitAnswer(user, this), 2000);
    }
    this.window.querySelector(".auto button").addEventListener("click", event => this.toggleAutoAnswer(user, quiz))
  }

  /**
   * @param {User} user 
   * @param {Quiz} quiz 
   */
  initOptions(user, quiz) {
    this.qabox.querySelectorAll("li a").forEach(async (element, index, array) => {
      element.removeAttribute("href");
      element.addEventListener("click", async event => quiz.submitAnswer(user, this, index + 1, true));
    });
  }

  /**
   * Toggle and trigger auto answer
   * @param {User} user 
   * @param {Quiz} quiz 
   */
  toggleAutoAnswer(user, quiz) {
    let autoAnswerStatus = this.window.querySelector(".auto span");
    autoAnswerStatus.classList.toggle("enable");

    if (!autoAnswerStatus.classList.contains("enable")) {
      GM_deleteValue("auto-answer");
      return;
    }

    GM_setValue("auto-answer", true);
    quiz.submitAnswer(user, this);
  }

  /**
   * Show quiz
   * @param {Quiz} quiz 
   */
  async showQuiz(quiz) {
    let original = this.window.querySelector(".original div");
    original.innerHTML = `題目編號:${quiz.sn}<br>原題目:${decodeURIComponent(quiz.question)}<ol><li>${quiz.options.map(decodeURIComponent).join("</li><li>")}</li></ol>`;
    original.classList.remove("hide");
    this.window.querySelector(".original button").classList.add("hide");

    let answer = await Database.answerOf(quiz).catch(err => null);
    let options = Array.from(this.window.querySelectorAll(".original li"));
    if (answer)
      options.forEach((element, index) => element.classList.add(index + 1 == answer ? "correct" : "wrong"));
  }

  /**
   * Show hint
   * @param {Quiz} quiz 
   */
  async showHint(quiz) {
    let view = this.window.querySelector(".hint span");
    let hint = await Database.hintOf(quiz).catch(err => null);
    this.window.querySelector(".hint button").classList.add("hide");
    if (!hint) {
      view.innerHTML = "題庫中無資料。";
      return;
    }
    view.innerHTML = "提示已獲取!";
    view.classList.add("success");
    this.qabox.querySelector(`li:nth-child(${hint}) a`).classList.add("wrong");
  }

  /**
   * Set response of database to view
   * @param {JSON} response 
   */
  setSubmitResult(response) {
    let messages = {
      "DATA_INVALID": "無法送出答案,請檢查是否有更新,或通知作者。",
      "DATA_APPENDED": "資料新增了!感謝提供~",
      "DATA_UPDATED": "資料更新了!感謝提供~",
      "IGNORED": "題庫中已經有資料啦~",
    };
    const statusDisplay = this.window.querySelector(".report span");
    statusDisplay.innerHTML = messages[response.message];
    if (response.success)
      statusDisplay.classList.add("success");
  }

  /**
   * Set native baha response of quiz into view
   * @param {String} html 
   */
  setResponse(html) {
    this.qabox.style["text-align"] = "center";
    this.qabox.innerHTML = html;
  }
}
(function () {
  'use strict';

  document.querySelector(".BH-qabox1").outerHTML += GM_getResourceText("quizrp");

  let user = new User();
  let quiz = new Quiz();
  let view = new View(user, quiz);
})();