import md5 from 'md5'; import _ from 'lodash'; import crypto from 'crypto'; import ZZZApiTool from './mysapi/tool.js'; import { randomString } from '../utils/data.js'; import MysApi from '../../genshin/model/mys/mysApi.js'; // const DEVICE_ID = randomString(32).toUpperCase() // const DEVICE_NAME = randomString(_.random(1, 10)); /** * 米游社ZZZAPI(继承自MysApi) */ export default class MysZZZApi extends MysApi { constructor(uid, cookie, option = {}) { super(uid, cookie, option, true); // 初始化 uid、server、apiTool this.uid = uid; this.server = this.getServer(uid); this.apiTool = new ZZZApiTool(uid, this.server); if (typeof this.cookie !== 'string' && this.cookie) { const ck = Object.values(this.cookie).find(item => { return item.ck && item.uid === uid; }); if (!ck) { throw new Error(`[ZZZ]要查询的UID:${uid}未绑定Cookie`); } this._device = ck?.device_id || ck?.device; this.cookie = ck?.ck; } if (!this._device) { this._device = crypto.randomUUID(); } } /** * 获取服务器 * @returns {string} */ getServer() { const _uid = this.uid?.toString(); if (_uid.length < 10) { return 'prod_gf_cn'; // 官服 } switch (_uid.slice(0, -8)) { case '10': return 'prod_gf_us'; // 美服 case '15': return 'prod_gf_eu'; // 欧服 case '13': return 'prod_gf_jp'; // 亚服 case '17': return 'prod_gf_sg'; // 港澳台服 } } /** * 获取请求网址 * @param {string} type * @param {object} data * @returns {object|boolean} */ getUrl(type, data = {}) { // 设置设备ID data.deviceId = this._device; // 获取请求地址 const urlMap = this.apiTool.getUrlMap(data); if (!urlMap[type]) return false; // 获取请求参数(即APITool中默认的请求参数,此参数理应是不可获取的,详细请参照 lib/mysapi/tool.js`) let { url, query = '', body = '', noDs = false, dsSalt = '', } = urlMap[type]; // 如果有query,拼接到url上 if (query) url += `?${query}`; // 如果传入了 query 参数,将 query 参数拼接到 url 上 if (data.query) { let str = ''; for (let key in data.query) { if (data.query[key] === undefined) continue; else if (data.query[key] === null) str += `${key}&`; else if (Array.isArray(data.query[key])) { data.query[key].forEach(item => { str += `${key}[]=${item}&`; }); } else str += `${key}=${data.query[key]}&`; } str = str.slice(0, -1); if (url.includes('?')) { url += `&${str}`; } else { url += `?${str}`; } } // 写入 body if (body) body = JSON.stringify(body); // 获取请求头 let headers = this.getHeaders(query, body); if (data.deviceFp) { headers['x-rpc-device_fp'] = data.deviceFp; // 兼容喵崽 this._device_fp = { data: { device_fp: data.deviceFp } }; } // 写入 cookie headers.cookie = this.cookie; // 写入设备ID if (this._device) { headers['x-rpc-device_id'] = this._device; } if (data?.deviceInfo && data?.modelName) { const deviceBrand = data.deviceInfo?.split('/')[0]; try { headers['x-rpc-device_name'] = `${deviceBrand} ${data.modelName}`; headers['x-rpc-device_model'] = data.modelName; headers['x-rpc-csm_source'] = 'myself'; } catch (error) { logger.error(`[ZZZ]设备信息解析失败:${error.message}`); } } // 写入DS switch (dsSalt) { case 'web': { headers.DS = this.getDS2(); break; } default: } if (type === 'zzzAuthKey') { let extra = { DS: this.getDS2(), Host: 'api-takumi.mihoyo.com', }; headers = Object.assign(headers, extra); } else { headers.DS = this.getDs(query, body); } // 如果不需要 DS,删除 DS if (noDs) { delete headers.DS; if (this._device) { body = JSON.parse(body); body.device_id = this._device; body = JSON.stringify(body); } } // 返回请求参数 return { url, headers, body }; } /** * 获取DS * @param {string} q * @param {string} b * @returns {string} */ getDs(q = '', b = '') { let n = ''; if (['prod_gf_cn'].includes(this.server)) { n = 'xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs'; } else { n = 'okr4obncj8bw5a65hbnn5oo6ixjc3l9w'; } const t = Math.round(new Date().getTime() / 1000); const r = Math.floor(Math.random() * 900000 + 100000); const DS = md5(`salt=${n}&t=${t}&r=${r}&b=${b}&q=${q}`); return `${t},${r},${DS}`; } /** * 获取DS2 * @returns {string} */ getDS2() { const t = Math.round(new Date().getTime() / 1000); const r = randomString(6); const sign = md5(`salt=WGtruoQrwczmsjLOPXzJLnaAYycsLavx&t=${t}&r=${r}`); return `${t},${r},${sign}`; } /** * 获取请求头 * @param {string} query * @param {string} body * @returns {object} */ getHeaders(query = '', body = '') { const cn = { app_version: '2.73.1', User_Agent: 'Mozilla/5.0 (Linux; Android 11; J9110 Build/55.2.A.4.332; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.179 Mobile Safari/537.36 miHoYoBBS/2.73.1', client_type: '5', Origin: 'https://act.mihoyo.com', X_Requested_With: 'com.mihoyo.hyperion', Referer: 'https://act.mihoyo.com/', }; const os = { app_version: '2.55.0', User_Agent: 'Mozilla/5.0 (Linux; Android 11; J9110 Build/55.2.A.4.332; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.179 Mobile Safari/537.36 miHoYoBBSOversea/2.55.0', client_type: '2', Origin: 'https://act.hoyolab.com', X_Requested_With: 'com.mihoyo.hoyolab', Referer: 'https://act.hoyolab.com/', }; let client; if (['prod_gf_cn'].includes(this.server)) { client = cn; } else { client = os; } return { 'x-rpc-app_version': client.app_version, // 'x-rpc-client_type': client.client_type, '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), Origin: client.Origin, }; } /** * 校验状态码 * @param e 消息e * @param res 请求返回 * @param type 请求类型 如 srNote * @param data 查询请求的数据 * @returns {Promise<*|boolean>} */ async checkCode(e, res, type, data = {}) { if (!res || !e) { e.reply('米游社接口请求失败,暂时无法查询'); return false; } this.e = e; this.e.isZZZ = true; res.retcode = Number(res.retcode); switch (res.retcode) { case 0: break; case 10102: if (res.message === 'Data is not public for the user') { this.e.reply(`\nUID:${this.uid},米游社数据未公开`, false, { at: this.userId, }); } else { this.e.reply(`UID:${this.uid},请先去米游社绑定角色`); } break; case 10041: case 5003: this.e.reply( `UID:${this.uid},米游社账号异常,暂时无法查询,请尝试%绑定设备` ); break; case 10035: case 1034: { let handler = this.e.runtime?.handler || {}; // 如果有注册的mys.req.err,调用 if (handler.has('mys.req.err')) { logger.mark( `[米游社zzz查询失败][UID:${this.uid}][qq:${this.userId}] 遇到验证码,尝试调用 Handler mys.req.err` ); res = (await handler.call('mys.req.err', this.e, { mysApi: this, type, res, data, mysInfo: this, })) || res; } if (!res || res?.retcode === 1034 || res?.retcode === 10035) { logger.mark( `[米游社zzz查询失败][UID:${this.uid}][qq:${this.userId}] 遇到验证码` ); this.e.reply('米游社zzz查询遇到验证码,请稍后再试'); } break; } default: if (/(登录|login)/i.test(res.message)) { logger.mark(`[ck失效][UID:${this.uid}]`); this.e.reply(`UID:${this.uid},米游社cookie已失效`); } else { this.e.reply( `米游社接口报错,暂时无法查询:${res.message || 'error'}` ); } break; } if (res.retcode !== 0) { logger.mark( `[米游社zzz接口报错]${JSON.stringify(res)},UID:${this.uid}` ); } return res; } /** * 获取米游社数据 * @param {*} e 消息e * @param {keyof ZZZApiTool['zzzUrlMap']} type 请求类型 * @param {{deviceFp: string; query: Record; headers: object;}} data * @param {boolean} cached */ async getFinalData(e, type, data = {}, cached = false) { if (data.deviceFp) { if (!data.headers) data.headers = {}; data.headers['x-rpc-device_fp'] = data.deviceFp; } // 从 this.cookie 中获取ltuid const ck = this.cookie; let ltuid = ck.match(/ltuid=(\d+);/); if (ltuid) { ltuid = ltuid[1]; } let bindInfo = await redis.get(`ZZZ:DEVICE_FP:${ltuid}:BIND`); if (bindInfo) { try { bindInfo = JSON.parse(bindInfo); data = { ...data, productName: bindInfo?.deviceProduct, deviceType: bindInfo?.deviceName, modelName: bindInfo?.deviceModel, oaid: bindInfo?.oaid, deviceInfo: bindInfo?.deviceFingerprint, board: bindInfo?.deviceBoard, }; } catch (error) { bindInfo = null; } } const result = await this.getData(type, data, cached); const _data = await this.checkCode(e, result, type, {}); if (!_data || _data.retcode !== 0) return false; return _data.data; } }