// ==UserScript==
// @name 巴哈姆特簽到腳本(每日、公會、動畫瘋)
// @namespace https://home.gamer.com.tw/moontai0724
// @match https://*.gamer.com.tw/*
// @grant GM_getResourceText
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect api.gamer.com.tw
// @connect guild.gamer.com.tw
// @connect script.google.com
// @connect script.googleusercontent.com
// @connect ani.gamer.com.tw
// @connect home.gamer.com.tw
// @noframes
// @version 6.0.1
// @description 打開巴哈自動跑所有簽到,包含動畫瘋、公會簽到、每日簽到等功能。
// @author moontai0724
// @homepage https://github.com/moontai0724/bahamut-sign-helper-script
// @supportURL https://github.com/moontai0724/bahamut-sign-helper-script/issues
// @resource resource_g66oet https://github.com/moontai0724/bahamut-sign-helper-script/raw/release/pages/animad-manual-answer/index.html
// ==/UserScript==
(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
})((function () { 'use strict';
const { info: originalInfo, error: originalError, ...remains } = console;
const prefix = `[bahamut-sign-helper-script]`;
function info(...params) {
return originalInfo(prefix, ...params);
}
function error(...params) {
return originalError(prefix, ...params);
}
var Logger = { error, info, ...remains };
var ScriptEvent;
(function (ScriptEvent) {
ScriptEvent["SystemInit"] = "system-init";
ScriptEvent["SystemRepliedResult"] = "system-replied-result";
ScriptEvent["UserAnswered"] = "user-answered";
ScriptEvent["UserClosed"] = "user-closed";
ScriptEvent["ViewMounted"] = "view-mounted";
})(ScriptEvent || (ScriptEvent = {}));
function on(scriptEvent, callback, target = window) {
const listener = (event) => {
if (event.data.scriptEvent !== scriptEvent)
return;
callback(event);
};
target.addEventListener("message", listener);
return listener;
}
function send(scriptEvent, content, target = window.parent) {
const context = {
content,
scriptEvent,
};
return target.postMessage(context, "*");
}
const TODAY = (() => {
const fullDate = new Date().toLocaleDateString("zh-TW", {
day: "2-digit",
month: "2-digit",
timeZone: "Asia/Taipei",
year: "numeric",
});
const [currentYear, currentMonth, currentDate] = fullDate.split("/");
const startOfTodayTimeString = `${currentYear}-${currentMonth}-${currentDate}T00:00:00+0800`;
return {
/**
* The day of today.
* @example 9
*/
day: parseInt(currentDate, 10),
/**
* The full date of today.
* @example "2024/03/09"
*/
full: fullDate,
/**
* The month of today.
* @example 3
*/
month: parseInt(currentMonth, 10),
/**
* The start time of today.
* @example new Date("2024-03-09T00:00:00+0800")
*/
start: new Date(startOfTodayTimeString),
/**
* The year of today.
* @example 2024
*/
year: parseInt(currentYear, 10),
};
})();
const BAHAID = (() => {
const account = /BAHAID=(?<BAHAID>.+?);/u.exec(document.cookie)?.[1];
if (!account) {
// directly break the script execution if the account is not found.
throw new Error("BAHAID is not found in cookie.");
}
return account;
})();
function getFullScreenIframe(html) {
const iframe = document.createElement("iframe");
iframe.srcdoc = html;
iframe.style.width = "100vw";
iframe.style.height = "100vh";
iframe.style.position = "fixed";
iframe.style.top = "0";
iframe.style.left = "0";
iframe.style.zIndex = "9999";
iframe.style.border = "none";
return iframe;
}
function loadFullScreenIframe(html) {
const iframe = getFullScreenIframe(html);
document.body.appendChild(iframe);
return iframe;
}
/* eslint-disable no-param-reassign, max-lines-per-function */
/**
* Get a recursive proxy option for the proxy object, which will auto trigger
* parent setter when the child setter is triggered.
* @param parentTarget parent of this item
* @param parentKey key of this item in the parent
* @returns a recursive proxy option for the proxy object
*/
function getOptions(parentTarget, parentKey) {
const handler = {
get(itemTarget, itemKey) {
const typedItemKey = itemKey;
// get current item
const itemValue = itemTarget[typedItemKey];
// if the item is an object, return a proxy
if (typeof itemValue === "object" && itemValue !== null) {
return new Proxy(itemValue, getOptions(new Proxy(itemTarget, handler), itemKey));
}
// return the item
return itemValue;
},
set(itemTarget, itemKey, itemValue) {
const typedItemKey = itemKey;
const typedItemValue = itemValue;
// update current item
itemTarget[typedItemKey] = typedItemValue;
// trigger parent setter
parentTarget[parentKey] = itemTarget;
// report success
return true;
},
};
return handler;
}
/**
* Defines the environment variables here. The key of first level object will be
* the identifier that stored into the script storage, and the value will be the
* default value if the value is not set before.
*/
const environmentVariables = {
config: {
animad: {
quiz: {
/**
* Whether to answer the quiz automatically.
* @default true
*/
autoAnswer: true,
/**
* The grace time of the quiz, calculated in milliseconds from the
* start of the day. The manual answer will not be triggered
* if the time is not reached.
*/
graceTime: 0,
/**
* Whether to use the source of the quiz.
*/
source: {
/**
* Fetch answer from daily post of user `blackxblue`.
* @see https://home.gamer.com.tw/blackxblue
*
* @default true
*/
blackxblue: true,
/**
* Fetch answer from unofficial gamer quiz collection.
* Since this approach is not stable, and is slow, this is only
* be used as a fallback.
*
* @see https://home.gamer.com.tw/creationDetail.php?sn=3924920
*
* @default true
*/
collection: true,
},
},
},
},
enable: {
/**
* Whether to enable the answer animad quiz feature.
*/
animadQuiz: true,
/**
* Whether to enable the daily sign feature.
* @default true
*/
dailySign: true,
/**
* Whether to enable the guild sign feature.
* @default true
*/
guildSign: true,
},
record: {},
};
/**
* A proxy object that will read and write the environment variables from the
* script storage. The value will be the default value if the value is not set
* in the script storage.
*/
const values = new Proxy(environmentVariables, {
get(target, key) {
const value = GM_getValue(key.toString(), target[key]);
if (typeof value === "object" && value !== null) {
return new Proxy(value, getOptions(values, key));
}
return value;
},
set(_, name, value) {
GM_setValue(name.toString(), value);
return true;
},
});
function getRecord() {
const record = values.record[BAHAID];
const defaultValue = {
updatedAt: TODAY.full,
};
if (!record || record.updatedAt !== TODAY.full) {
values.record[BAHAID] = defaultValue;
return values.record[BAHAID];
}
return record;
}
/**
* Generate a random hexadecimal string
*
* @param length The length of the hexadecimal string
*/
function generateRandomHex(length = 16) {
// Initialize an empty string to store the hexadecimal value
let hexString = "";
// Loop through the length of the hexadecimal string
// eslint-disable-next-line no-plusplus
for (let index = 0; index < length; index++) {
// Generate a random hexadecimal digit (0-9, A-F)
const randomDigit = Math.floor(Math.random() * 16).toString(16);
// Append the random digit to the hexadecimal string
hexString += randomDigit;
}
// Return the generated hexadecimal string
return hexString;
}
/**
* Get the CSRF token from `ckBahamutCsrfToken` cookie or generate a random one.
*
* @returns The CSRF token from cookie or a random 16-digit hexadecimal string
*/
function getCSRFToken() {
const existing = document.cookie
.split(";")
.find(value => value.trim().startsWith("ckBahamutCsrfToken"));
if (!existing)
return generateRandomHex();
const [, ...fragments] = existing.split("=");
const value = fragments.join("=");
return value;
}
var HeaderOption;
(function (HeaderOption) {
HeaderOption["ContentType"] = "Content-Type";
HeaderOption["XBahamutCsrfToken"] = "x-bahamut-csrf-token";
})(HeaderOption || (HeaderOption = {}));
var ContentType;
(function (ContentType) {
ContentType["FormData"] = "multipart/form-data";
ContentType["FormUrlEncoded"] = "application/x-www-form-urlencoded";
})(ContentType || (ContentType = {}));
/**
* Send an HTTP request by using `GM_xmlhttpRequest`.
*
* @param method HTTP method, only GET, HEAD and POST are supported.
* @param url Target URL.
* @param options Options for `GM_xmlhttpRequest`.
*
* @returns A promise that wrap resolves to the response.
*/
async function request(method, url, options) {
return new Promise((resolve, reject) => {
const basicOptions = {
method,
onerror: reject,
onload: httpResponse => {
resolve(httpResponse.response);
},
ontimeout: reject,
url,
};
Object.assign(basicOptions, options);
GM_xmlhttpRequest(basicOptions);
});
}
/**
* Send an HTTP GET request by using `GM_xmlhttpRequest`.
*
* @param url Target URL.
* @param options Options for `GM_xmlhttpRequest`.
*/
async function get(url, options = {}) {
return request("GET", url, options);
}
/**
* Send an HTTP POST request by using `GM_xmlhttpRequest`.
*
* @param url Target URL.
* @param options Options for `GM_xmlhttpRequest`.
*/
async function post(url, options = {}) {
return request("POST", url, options);
}
let quiz;
/**
* Get the quiz of the day.
* @throws {ErrorResponse} If the quiz is answered today.
*/
async function getQuiz() {
if (quiz)
return quiz;
const uri = "https://ani.gamer.com.tw/ajax/animeGetQuestion.php";
const response = await get(uri, {
responseType: "json",
});
if ("error" in response)
return Promise.reject(response);
quiz = response;
return response;
}
/**
* Submit the answer of the quiz.
* @param token By default, it will auto fetch from cached quiz. You can provide
* the token manually instead.
* @throws {ErrorResponse} When there are error message from api.
*/
async function submitAnswer$1(answer) {
const token = quiz?.token || (await getQuiz()).token;
const uri = "https://ani.gamer.com.tw/ajax/animeAnsQuestion.php";
const params = new URLSearchParams();
params.append("ans", answer.toString());
params.append("token", token);
params.append("t", Date.now().toString());
const response = await post(uri, {
data: params,
responseType: "json",
});
if ("error" in response)
return Promise.reject(response);
return response;
}
/**
* This is the api that the current landing page uses.
*/
async function check() {
const token = getCSRFToken();
const params = new FormData();
params.append("action", "2");
const response = await post("https://api.gamer.com.tw/user/v1/signin.php", {
anonymous: false,
cookie: `ckBahamutCsrfToken=${token}`,
data: params,
headers: {
[HeaderOption.XBahamutCsrfToken]: token,
},
responseType: "json",
});
if ("error" in response)
return Promise.reject(response.error);
return response.data;
}
/**
* This is the api that the current landing page uses.
* @example {
"days": 111,
"dialog": "",
"prjSigninDays": 0,
"btnMessage": "<i class=\"material-icons\">check_box</i>每日簽到已達成",
"totalWeeks": 4,
"dialogInfo": []
}
*/
async function sign$3() {
const token = getCSRFToken();
const params = new FormData();
params.append("action", "1");
const response = await post("https://api.gamer.com.tw/user/v1/signin.php", {
anonymous: false,
cookie: `ckBahamutCsrfToken=${token}`,
data: params,
headers: {
[HeaderOption.XBahamutCsrfToken]: token,
},
responseType: "json",
});
if ("error" in response)
return Promise.reject(response.error);
return response.data;
}
async function getMyGuilds() {
const uri = "https://api.gamer.com.tw/guild/v2/guild_my.php";
const response = await get(uri, {
anonymous: false,
// cookie: `ckBahamutCsrfToken=${token}`,
// data: params as unknown as string,
// headers: {
// [HeaderOption.XBahamutCsrfToken]: token,
// },
responseType: "json",
});
return response.data.list;
}
async function sign$2(id) {
const uri = "https://guild.gamer.com.tw/ajax/guildSign.php";
const data = new URLSearchParams();
data.set("sn", id.toString());
const response = await post(uri, {
anonymous: false,
data: data,
responseType: "json",
});
if ("error" in response)
return Promise.reject(response);
return response;
}
async function list(user, options = {}) {
const uri = "https://api.gamer.com.tw/home/v2/creation_list.php";
const params = new URLSearchParams({
owner: user,
...options,
});
const response = await get(`${uri}?${params.toString()}`, { responseType: "json" });
return response.data.list;
}
async function getHTML(sn) {
const uri = "https://api.gamer.com.tw/mobile_app/bahamut/v1/home_creation_detail_webview.php";
const params = new URLSearchParams({ sn });
const response = await get(`${uri}?${params.toString()}`);
return response;
}
async function find(question) {
const uri = "https://script.google.com/macros/s/AKfycbxYKwsjq6jB2Oo0xwz4bmkd3-5hdguopA6VJ5KD/exec";
const searchParams = new URLSearchParams({
question,
type: "quiz",
});
const response = await get(`${uri}?${searchParams.toString()}`, {
responseType: "json",
});
if (!response.success || !response.data)
throw new Error("Failed to find the quiz.");
return response.data;
}
function isToday(title) {
const regex = /(\d{1,2})[/-](\d{1,2})/;
const match = title.match(regex);
if (!match)
return false;
const [, month, day] = match;
const isMonthEqual = parseInt(month, 10) === TODAY.month;
const isDayEqual = parseInt(day, 10) === TODAY.day;
return isMonthEqual && isDayEqual;
}
function findAnswer(html) {
const element = document.createElement("html");
element.innerHTML = html;
const postContent = element.querySelector("#home_content")?.textContent;
if (!postContent)
throw new Error("No post content found.");
const answer = postContent.match(/[aAA]\s*.\s*([1-41-4])/)?.[1];
if (!answer)
throw new Error("No answer found.");
return parseInt(answer, 10);
}
async function fromblackxblue() {
const creations = await list("blackXblue");
const todayCreation = creations.find(creation => isToday(creation.title));
if (!todayCreation)
throw new Error("No creation found.");
const html = await getHTML(creations[0].csn);
const answer = findAnswer(html);
return answer;
}
async function fromCollection() {
if (!values.config.animad.quiz.source.collection) {
const message = "The quiz collection answer source is disabled.";
Logger.info(message);
throw new Error(message);
}
const quiz = await getQuiz();
const result = await find(quiz.question);
if (!result.answer)
throw new Error("Failed to find the answer from the quiz collection.");
return result.answer;
}
async function getAnswer() {
if (!values.config.animad.quiz.autoAnswer) {
Logger.info("Auto answer for animad quiz is disabled.");
throw new Error("Auto answer for animad quiz is disabled.");
}
try {
const answer = await fromblackxblue();
Logger.info("Got the answer from blackxblue:", answer);
return answer;
}
catch (error) {
Logger.error("Failed to get the answer from blackxblue.", error);
}
try {
const answer = await fromCollection();
Logger.info("Got the answer from the quiz collection:", answer);
return answer;
}
catch {
Logger.error("Failed to get the answer from the quiz collection.");
}
throw new Error("Failed to get any answer.");
}
function setAnswerStatus(status) {
const existing = getRecord();
existing.animadQuizAnswered = status;
return existing;
}
async function isTodayAnswered() {
const record = getRecord();
const answered = !!record.animadQuizAnswered;
if (answered)
return true;
try {
await getQuiz();
}
catch {
setAnswerStatus(true);
return true;
}
return false;
}
async function submitAnswer(answer) {
const result = await submitAnswer$1(answer).catch(error => {
if (error.msg === "答題錯誤")
return error;
if (error.msg === "今日已經答過題目了,一天僅限一次機會")
return error;
throw error;
});
setAnswerStatus(true);
return result;
}
var html = GM_getResourceText("resource_g66oet");
let iframe;
function close() {
iframe.remove();
Logger.info("Manual answer animad quiz closed.");
}
async function onUserAnswered(event) {
Logger.info("User answered manual answer animad quiz.", event);
const answer = event.data.content;
const result = await submitAnswer$1(answer).catch(error => ({
gift: error.msg || error.message,
ok: 0,
}));
send(ScriptEvent.SystemRepliedResult, result.gift, event.source);
}
async function init$3() {
const quiz = await getQuiz();
Logger.info("Initiating manual answer animad quiz...");
on(ScriptEvent.ViewMounted, event => {
Logger.info("Manual answer animad quiz view mounted.");
send(ScriptEvent.SystemInit, { question: quiz }, event.source);
});
on(ScriptEvent.UserClosed, close);
on(ScriptEvent.UserAnswered, onUserAnswered);
iframe = loadFullScreenIframe(html);
}
async function performAutoAnswer() {
const answer = await getAnswer();
return submitAnswer(answer);
}
function graceTimePassed() {
const { graceTime } = values.config.animad.quiz;
if (graceTime <= 0)
return true;
const now = new Date();
return now.getTime() - TODAY.start.getTime() >= graceTime;
}
async function performManualAnswer() {
return init$3();
}
// 動畫瘋答題感謝 maple3142/動畫瘋工具箱 支援:https://gf.qytechs.cn/zh-TW/scripts/39136
async function init$2() {
try {
if (!values.enable.animadQuiz) {
Logger.info("Animad quiz feature is disabled.");
return;
}
if (await isTodayAnswered()) {
Logger.info("Animad quiz is already answered.");
return;
}
try {
const result = await performAutoAnswer();
Logger.info("Successfully auto answered the animad quiz.", result);
}
catch {
Logger.info("Failed to auto answer the animad quiz, fallback to manual answer.");
if (!graceTimePassed()) {
Logger.info("Grace time is not passed, aborting manual answer for animad quiz.");
return;
}
await performManualAnswer();
}
}
catch (error) {
Logger.error("Encountered an error while performing animad quiz:", error);
}
}
function setSignStatus(status) {
const existing = getRecord();
existing.dailySigned = status;
return existing;
}
async function isTodaySigned() {
const record = getRecord();
if (!record.dailySigned) {
const status = await check();
const signed = !!status.signin;
setSignStatus(signed);
return signed;
}
return !!record.dailySigned;
}
async function sign$1() {
const signResult = await sign$3();
setSignStatus(true);
return signResult;
}
async function init$1() {
try {
if (!values.enable.dailySign) {
Logger.info("Daily sign feature is disabled.");
return;
}
if (await isTodaySigned()) {
Logger.info("Daily sign is already performed.");
return;
}
const signResult = await sign$1();
Logger.info("Successfully performed daily sign!", signResult);
}
catch (error) {
Logger.error("Encountered an error while performing daily sign:", error);
}
}
async function getMyGuildIds() {
const infos = await getMyGuilds();
const ids = infos.map(info => info.sn);
return ids;
}
function isNotSignedYet(id) {
const record = getRecord();
return !record.signedGuilds?.includes(id);
}
async function getUnsignedGuildIds() {
const ids = await getMyGuildIds();
const unsignedIds = ids.filter(isNotSignedYet);
return unsignedIds;
}
async function setSigned(id) {
const existing = getRecord();
if (!existing.signedGuilds)
existing.signedGuilds = [];
if (existing.signedGuilds.includes(id))
return existing;
existing.signedGuilds.push(id);
return existing;
}
async function sign(id) {
if (!isNotSignedYet(id)) {
throw new Error(`Guild ${id} is already signed today.`);
}
const signResult = await sign$2(id).catch(error => {
if (error.msg === "您今天已經簽到過了!") {
return Promise.resolve(error);
}
return Promise.reject(error);
});
setSigned(id);
return signResult;
}
async function init() {
try {
const ids = await getUnsignedGuildIds();
if (!ids.length) {
Logger.info("All guild sign is already performed.");
return;
}
const results = await Promise.allSettled(ids.map(sign));
Logger.info("Successfully performed guild sign!", results);
}
catch (error) {
Logger.error("Encountered an error while performing guild sign:", error);
}
}
init$1();
init();
init$2();
}));