Greasy Fork

Greasy Fork is available in English.

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

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

当前为 2022-01-27 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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);
})();