From 51fb65cdb4efd37cd9f052714381f538db4dcf2e Mon Sep 17 00:00:00 2001 From: bietiaop <1527109126@qq.com> Date: Mon, 8 Jul 2024 22:17:31 +0800 Subject: [PATCH] gacha --- .gitignore | 2 + apps/gachalog.js | 84 +++++++++++++++++++---------- data/.gitkeep | 0 lib/authkey.js | 9 ++-- lib/db.js | 59 +++++++++++++++++++++ lib/gacha.js | 129 +++++++++++++++++++++++++++++++++++++++++++++ lib/mysapi.js | 31 +++++------ lib/mysapi/tool.js | 18 +++++-- model/gacha.js | 29 +++++++++- utils/file.js | 6 +++ utils/time.js | 2 + 11 files changed, 313 insertions(+), 56 deletions(-) create mode 100644 data/.gitkeep create mode 100644 lib/db.js create mode 100644 lib/gacha.js create mode 100644 utils/file.js diff --git a/.gitignore b/.gitignore index 7d90528..e2c293e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ config/**/*.* .DS_Store +data/**/*.* +!data/.gitkeep diff --git a/apps/gachalog.js b/apps/gachalog.js index 609b0d3..52205d5 100644 --- a/apps/gachalog.js +++ b/apps/gachalog.js @@ -3,6 +3,8 @@ import _ from 'lodash'; import render from '../lib/render.js'; import { ZZZNoteResp } from '../model/note.js'; import { rulePrefix } from '../lib/common.js'; +import { getAuthKey, getStoken } from '../lib/authkey.js'; +import { updateGachaLog } from '../lib/gacha.js'; export class GachaLog extends ZZZPlugin { constructor() { @@ -13,41 +15,69 @@ export class GachaLog extends ZZZPlugin { priority: 100, rule: [ { - reg: `${rulePrefix}抽卡记录$`, - fnc: 'gachaLog', + reg: `${rulePrefix}抽卡链接$`, + fnc: 'startGachaLog', + }, + { + reg: `${rulePrefix}刷新抽卡链接$`, + fnc: 'refreshGachaLog', + }, + { + reg: `^${rulePrefix}抽卡帮助$`, + fnc: 'gachaHelp', }, ], }); } - async gachaLog(e) { - const { api, deviceFp } = await this.getAPI(); - if (!api) return false; - let userData = await api.getData('zzzUser'); - if (!userData?.data || _.isEmpty(userData.data.list)) { - await e.reply('[zzznote]玩家信息获取失败'); + async gachaHelp() { + const reply_msg = [ + 'StarRail-Plugin 抽卡链接绑定方法:', + '1. 输入【#zzz抽卡链接】,等待 bot 回复【请发送抽卡链接】', + '2. 获取抽卡链接', + '3. 将获取到的抽卡链接发送给 bot', + ].join('\n'); + await this.reply(reply_msg); + } + async startGachaLog() { + if (!this.e.isPrivate) { + await this.reply('请私聊发送抽卡链接', false, { at: true }); return false; } - userData = userData?.data?.list[0]; - let noteData = await api.getData('zzzNote', { deviceFp }); - noteData = await api.checkCode(e, noteData, 'zzzNote', {}); - if (!noteData || noteData.retcode !== 0) { - await e.reply('[zzznote]每日数据获取失败'); + this.setContext('gachaLog'); + await this.reply('请发送抽卡链接', false, { at: true }); + } + async gachaLog() { + if (!this.e.isPrivate) { + await this.reply('请私聊发送抽卡链接', false, { at: true }); return false; } - noteData = noteData.data; - noteData = new ZZZNoteResp(noteData); - let avatar = this.e.bot.avatar; - // 头像 - if (this.e.member?.getAvatarUrl) { - avatar = await this.e.member.getAvatarUrl(); - } else if (this.e.friend?.getAvatarUrl) { - avatar = await this.e.friend.getAvatarUrl(); + let key = this.e.msg.trim(); + key = key?.split?.('authkey=')?.[1]?.split('&')?.[0]; + if (!key) { + await this.reply('抽卡链接格式错误,请重新发送'); + this.finish('gachaLog'); + return false; } - const finalData = { - avatar, - player: userData, - note: noteData, - }; - await render(e, 'note/index.html', finalData); + this.finish('gachaLog'); + this.getLog(key); + } + async refreshGachaLog() { + const uid = await this.getUID(); + const key = await getAuthKey(this.e, uid); + if (!key) { + await this.reply('authKey获取失败,请检查cookie是否过期'); + return false; + } + this.getLog(key); + } + async getLog(key) { + const uid = await this.getUID(); + const data = await updateGachaLog(key, uid); + let msg = `抽卡记录更新成功,共${Object.keys(data).length}个卡池`; + for (const name in data) { + msg += `\n${name}一共${data[name].length}条记录`; + } + await this.reply(msg); + return false; } } diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/authkey.js b/lib/authkey.js index 12a775a..66d3fb3 100644 --- a/lib/authkey.js +++ b/lib/authkey.js @@ -15,16 +15,15 @@ export async function getAuthKey(e, zzzUid, authAppid = 'csc') { if (!User) { throw new Error('未安装逍遥插件,无法自动刷新抽卡链接'); } - let user = new User(e); - // set genshin uid + const user = new User(e); await user.getCookie(e); let ck = await user.getStoken(e.user_id); ck = `stuid=${ck.stuid};stoken=${ck.stoken};mid=${ck.mid};`; - let api = new MysZZZApi(zzzUid, ck); - let type = 'zzzPayAuthKey'; + const api = new MysZZZApi(zzzUid, ck); + let type = 'zzzAuthKey'; switch (authAppid) { case 'csc': { - type = 'zzzPayAuthKey'; + type = 'zzzAuthKey'; break; } default: diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..058c243 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,59 @@ +import { readFileSync, writeFileSync } from 'fs'; +import path from 'path'; +import { checkFolderExistAndCreate } from '../utils/file.js'; +import { dataPath } from './path.js'; +const dbPath = { + gacha: 'gacha', +}; + +/** + * + * @param {string} dbName + * @param {string} dbFile + * @returns {object} + */ +export function getDB(dbName, dbFile) { + const db = dbPath[dbName]; + const dbFolder = path.join(dataPath, db); + try { + const dbPath = path.join(dbFolder, `${dbFile}.json`); + return JSON.parse(readFileSync(dbPath, 'utf-8')); + } catch (error) { + logger.mark(`读取数据库失败: ${error.message}`); + return null; + } +} + +/** + * + * @param {string} dbName + * @param {string} dbFile + * @param {object} data + */ +export function setDB(dbName, dbFile, data) { + const db = dbPath[dbName]; + const dbFolder = path.join(dataPath, db); + try { + checkFolderExistAndCreate(dbFolder); + const dbPath = path.join(dbFolder, `${dbFile}.json`); + writeFileSync(dbPath, JSON.stringify(data, null, 2)); + } catch (error) { + logger.mark(`读取数据库失败: ${error.message}`); + } +} + +/** + * @param {string} uid + * @returns {object} + */ +export function getGachaLog(uid) { + return getDB('gacha', uid); +} + +/** + * @param {string} uid + * @param {object} data + */ +export function saveGachaLog(uid, data) { + setDB('gacha', uid, data); +} diff --git a/lib/gacha.js b/lib/gacha.js new file mode 100644 index 0000000..b70beb8 --- /dev/null +++ b/lib/gacha.js @@ -0,0 +1,129 @@ +import { SingleGachaLog, ZZZGachaLogResp } from '../model/gacha.js'; +import { sleep } from '../utils/time.js'; +import { getGachaLog, saveGachaLog } from './db.js'; +import { ZZZ_GET_GACHA_LOG_API } from './mysapi/api.js'; + +export const gacha_type_meta_data = { + 音擎频段: ['3001'], + 独家频段: ['2001'], + 常驻频段: ['1001'], + 邦布频段: ['5001'], +}; +/** + * + * @param {string} authKey + * @param {*} gachaType + * @param {*} initLogGachaBaseType + * @param {number} page + * @param {string} endId + * @returns {Promise} + */ +export async function getZZZGachaLogByAuthkey( + authKey, + gachaType = '2001', + initLogGachaBaseType = '2', + page = 1, + endId = '0' +) { + const serverId = 'prod_gf_cn'; + const url = ZZZ_GET_GACHA_LOG_API; + const timestamp = Math.floor(Date.now() / 1000); + + const params = new URLSearchParams({ + authkey_ver: '1', + sign_type: '2', + auth_appid: 'webview_gacha', + init_log_gacha_type: gachaType, + init_log_gacha_base_type: initLogGachaBaseType, + gacha_id: '2c1f5692fdfbb733a08733f9eb69d32aed1d37', + timestamp: timestamp.toString(), + lang: 'zh-cn', + device_type: 'mobile', + plat_type: 'ios', + region: serverId, + authkey: authKey, + game_biz: 'nap_cn', + gacha_type: gachaType, + real_gacha_type: initLogGachaBaseType, + page: page, + size: '20', + end_id: endId, + }); + + const response = await fetch(`${url}?${params}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (!data || !data?.data) return null; + + return new ZZZGachaLogResp(data.data); +} + +/** + * + * @param {string} authKey + * @param {string} uid + * @returns {Promise<{ + * [x: string]: SingleGachaLog[]; + * }>} + */ +export async function updateGachaLog(authKey, uid) { + let previousLog = getGachaLog(uid); + if (!previousLog) { + previousLog = {}; + } + for (const name in gacha_type_meta_data) { + if (!previousLog[name]) { + previousLog[name] = []; + } + previousLog[name] = previousLog[name].map( + i => + new SingleGachaLog( + i.uid, + i.gacha_id, + i.gacha_type, + i.item_id, + i.count, + i.time, + i.name, + i.lang, + i.item_type, + i.rank_type, + i.id + ) + ); + const lastSaved = previousLog[name]?.[0]; + let page = 1; + let endId = '0'; + for (const type of gacha_type_meta_data[name]) { + queryLabel: while (true) { + const log = await getZZZGachaLogByAuthkey( + authKey, + type, + type[0], + page, + endId + ); + if (!log || !log?.list || log?.list?.length === 0) { + break; + } + for (const item of log.list) { + if (lastSaved && lastSaved.equals(item)) { + break queryLabel; + } + previousLog[name].push(item); + } + endId = log.list[log.list.length - 1]?.id || endId; + page++; + await sleep(400); + } + } + } + saveGachaLog(uid, previousLog); + return previousLog; +} diff --git a/lib/mysapi.js b/lib/mysapi.js index 6cb320b..8a4ee30 100644 --- a/lib/mysapi.js +++ b/lib/mysapi.js @@ -1,8 +1,9 @@ -import MysApi from '../../genshin/model/mys/mysApi.js'; import md5 from 'md5'; import _ from 'lodash'; import crypto from 'crypto'; import ZZZApiTool from './mysapi/tool.js'; +import MysApi from '../../genshin/model/mys/mysApi.js'; + // const DEVICE_ID = randomString(32).toUpperCase() const DEVICE_NAME = randomString(_.random(1, 10)); const game_region = [ @@ -79,20 +80,9 @@ export default class MysZZZApi extends MysApi { } default: } - if (type === 'zzzPayAuthKey') { + if (type === 'zzzAuthKey') { let extra = { - 'x-rpc-app_version': '2.40.1', - 'User-Agent': 'okhttp/4.8.0', - 'x-rpc-client_type': '5', - Referer: 'https://app.mihoyo.com', - Origin: 'https://webstatic.mihoyo.com', - // Cookie: this.cookies, - // DS: this.getDS2(), - 'x-rpc-sys_version': '12', - 'x-rpc-channel': 'mihoyo', - 'x-rpc-device_id': this._device, - 'x-rpc-device_name': DEVICE_NAME, - 'x-rpc-device_model': 'Mi 10', + DS: this.getDS2(), Host: 'api-takumi.mihoyo.com', }; headers = Object.assign(headers, extra); @@ -114,7 +104,7 @@ export default class MysZZZApi extends MysApi { let n = ''; if (['prod_gf_cn', 'prod_qd_cn'].includes(this.server)) { n = 'xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs'; - } else if (/official/.test(this.server)) { + } else if (/prod_gf_/.test(this.server)) { n = 'okr4obncj8bw5a65hbnn5oo6ixjc3l9w'; } let t = Math.round(new Date().getTime() / 1000); @@ -126,15 +116,15 @@ export default class MysZZZApi extends MysApi { getDS2() { let t = Math.round(new Date().getTime() / 1000); let r = randomString(6); - let sign = md5(`salt=jEpJb9rRARU2rXDA9qYbZ3selxkuct9a&t=${t}&r=${r}`); + let sign = md5(`salt=BIPaooxbWZW02fGHZL1If26mYCljPgst&t=${t}&r=${r}`); return `${t},${r},${sign}`; } getHeaders(query = '', body = '') { const cn = { - app_version: '2.44.1', + app_version: '2.63.1', User_Agent: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.44.1', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.63.1', client_type: '5', Origin: 'https://webstatic.mihoyo.com', X_Requested_With: 'com.mihoyo.hyperion', @@ -158,7 +148,10 @@ export default class MysZZZApi extends MysApi { return { 'x-rpc-app_version': client.app_version, 'x-rpc-client_type': client.client_type, - // 'x-rpc-page': '3.1.3_#/rpg', + 'User-Agent': 'okhttp/4.8.0', + 'x-rpc-sys_version': '12', + 'x-rpc-client_type': '2', + 'x-rpc-channel': 'mihoyo', 'User-Agent': client.User_Agent, Referer: client.Referer, DS: this.getDs(query, body), diff --git a/lib/mysapi/tool.js b/lib/mysapi/tool.js index 96d8f21..8d6aa63 100644 --- a/lib/mysapi/tool.js +++ b/lib/mysapi/tool.js @@ -11,7 +11,7 @@ export default class ZZZApiTool { */ constructor(uid, server) { this.uid = uid; - this.isSr = true; + this.isZZZ = true; this.server = server; this.game = 'zzz'; this.uuid = crypto.randomUUID(); @@ -19,18 +19,18 @@ export default class ZZZApiTool { getUrlMap = (data = {}) => { let host, hostRecord, hostPublicData; - if (['prod_gf_cn', 'prod_qd_cn'].includes(this.server)) { + if (['prod_gf_cn'].includes(this.server)) { host = 'https://api-takumi.mihoyo.com/'; hostRecord = 'https://api-takumi-record.mihoyo.com/'; hostPublicData = 'https://public-data-api.mihoyo.com/'; - } else if (/official/.test(this.server)) { + } else if (/prod_gf_/.test(this.server)) { host = 'https://sg-public-api.hoyolab.com/'; hostRecord = 'https://bbs-api-os.hoyolab.com/'; hostPublicData = 'https://sg-public-data-api.hoyoverse.com/'; } let urlMap = { zzz: { - ...(['prod_gf_cn', 'prod_qd_cn'].includes(this.server) + ...(['prod_gf_cn'].includes(this.server) ? { zzzUser: { url: `${host}binding/api/getUserGameRolesByCookie`, @@ -78,6 +78,16 @@ export default class ZZZApiTool { url: `${hostRecord}event/game_record_zzz/api/zzz/index`, query: `role_id=${this.uid}&server=${this.server}`, }, + zzzAuthKey: { + url: `${host}binding/api/genAuthKey`, + body: { + auth_appid: 'webview_gacha', + game_biz: 'nap_cn', + game_uid: this.uid * 1, + region: this.server, + }, + dsSalt: 'web', + }, }, }; return urlMap[this.game]; diff --git a/model/gacha.js b/model/gacha.js index 3f379a5..e5588d4 100644 --- a/model/gacha.js +++ b/model/gacha.js @@ -40,6 +40,18 @@ export class SingleGachaLog { this.rank_type = rank_type; this.id = id; } + + /** + * + * @param {SingleGachaLog} item + */ + equals(item) { + return ( + this.uid === item.uid && + this.gacha_id === item.gacha_id && + this.gacha_type === this.gacha_type + ); + } } /** @@ -59,7 +71,22 @@ export class ZZZGachaLogResp { const { page, size, list, region, region_time_zone } = data; this.page = page; this.size = size; - this.list = list; + this.list = list.map( + item => + new SingleGachaLog( + item.uid, + item.gacha_id, + item.gacha_type, + item.item_id, + item.count, + item.time, + item.name, + item.lang, + item.item_type, + item.rank_type, + item.id + ) + ); this.region = region; this.region_time_zone = region_time_zone; } diff --git a/utils/file.js b/utils/file.js new file mode 100644 index 0000000..6ad4a36 --- /dev/null +++ b/utils/file.js @@ -0,0 +1,6 @@ +import fs from 'fs'; +export function checkFolderExistAndCreate(folderPath) { + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } +} diff --git a/utils/time.js b/utils/time.js index 6e5201e..99dcedb 100644 --- a/utils/time.js +++ b/utils/time.js @@ -4,3 +4,5 @@ export const converSecondsToHM = seconds => { const mm = d.getUTCMinutes(); return [hh, mm]; }; + +export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));