Miao-Yunzai/plugins/genshin/model/gachaLog.js

704 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import base from './base.js'
import fetch from 'node-fetch'
import lodash from 'lodash'
import fs from 'node:fs'
import common from '../../../lib/common/common.js'
import gsCfg from './gsCfg.js'
export default class GachaLog extends base {
constructor (e) {
super(e)
this.model = 'gachaLog'
this.urlKey = `${this.prefix}url:`
/** 绑定的uid */
this.uidKey = `Yz:genshin:mys:qq-uid:${this.userId}`
this.path = `./data/gachaJson/${this.e.user_id}/`
this.pool = [
{ type: 301, typeName: '角色' },
{ type: 302, typeName: '武器' },
{ type: 200, typeName: '常驻' }
]
}
async logUrl () {
let url = this.e.msg
/** 处理url */
let param = this.dealUrl(url)
if (!param) return
if (!await this.checkUrl(param)) return
this.e.reply('链接发送成功,数据获取中... 请耐心等待')
/** 按卡池更新记录 */
for (let i in this.pool) {
this.type = this.pool[i].type
this.typeName = this.pool[i].typeName
let res = await this.updateLog()
if (res) await this.e.reply(`${this.typeName}记录获取成功,更新${res.num}`)
if (i <= 1) await common.sleep(500)
}
await this.e.reply('抽卡记录更新完成,您还可回复\n【#武器记录】统计武器池数据\n【#角色统计】按卡池统计数据\n【#导出记录】导出记录数据')
this.isLogUrl = true
this.all = []
let data = await this.getLogData()
this.e.msg = `[uid:${this.uid}]`
return data
}
async logFile () {
let url = await this.downFile()
if (!url) {
if (this.e?.file?.name.includes('output')) {
await this.e.reply('请先游戏里打开抽卡记录页面,再发送文件')
return true
}
return false
}
this.e.msg = url
return this.logUrl()
}
dealUrl (url) {
// timestamp=1641338980〈=zh-cn 修复链接有奇怪符号
url = url.replace(/〈=/g, '&').split('getGachaLog?')[1]
// 处理参数
let arr = new URLSearchParams(url).entries()
let params = {}
for (let val of arr) {
params[val[0]] = val[1]
}
if (!params.authkey) {
this.e.reply('链接复制错误')
return false
}
// 去除#/,#/log
params.authkey = params.authkey.replace(/#\/|#\/log/g, '')
return params
}
async downFile () {
this.creatFile()
let textPath = `${this.path}output_log.txt`
// 获取文件下载链接
let fileUrl = await this.e.friend.getFileUrl(this.e.file.fid)
let ret = await common.downFile(fileUrl, textPath)
if (!ret) {
this.e.reply('下载日志文件错误')
return false
}
// 读取txt文件
let txt = fs.readFileSync(textPath, 'utf-8')
let url = txt.match(/auth_appid=webview_gacha(.*)hk4e_cn/)
/** 删除文件 */
fs.unlink(textPath, () => {})
if (!url || !url[0]) {
return false
}
return url[0]
}
async checkUrl (param) {
if (!param.region) {
this.e.reply('链接参数错误缺少region\n请复制完整链接')
return false
}
let res = await this.logApi({
size: 6,
authkey: param.authkey,
region: param.region
})
if (res.retcode == -109) {
await this.e.reply('2.3版本后,反馈的链接已无法查询!请用安卓方式获取链接')
return false
}
if (res.retcode == -101) {
await this.e.reply('该链接已失效,请重新进入游戏,重新复制链接')
return false
}
if (res.retcode == 400) {
await this.e.reply('获取数据错误')
return false
}
if (res.retcode == -100) {
if (this.e.msg.length == 1000) {
await this.e.reply('输入法限制,链接复制不完整,请更换输入法复制完整链接')
return false
}
await this.e.reply('链接不完整,请长按全选复制全部内容(可能输入法复制限制),或者复制的不是历史记录页面链接')
return false
}
if (res.retcode != 0) {
await this.e.reply('链接复制错误')
return false
}
if (res?.data?.list && res.data.list.length > 0) {
this.uid = res.data.list[0].uid
await redis.setEx(this.uidKey, 3600 * 24 * 30, String(this.uid))
/** 保存authkey */
await redis.setEx(`${this.urlKey}${this.uid}`, 86400, param.authkey)
return true
} else {
await this.e.reply('暂无数据,请等待记录后再查询')
return false
}
}
async logApi (param) {
// 调用一次接口判断链接是否正确
let logUrl = 'https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog?'
/** 国际服 */
if (!['cn_gf01', 'cn_qd01'].includes(param.region)) {
logUrl = 'https://hk4e-api-os.mihoyo.com/event/gacha_info/api/getGachaLog?'
}
let logParam = new URLSearchParams({
authkey_ver: 1,
lang: 'zh-cn', // 只支持简体中文
gacha_type: 301,
page: 1,
size: 20,
end_id: 0,
...param
}).toString()
let res = await fetch(logUrl + logParam).catch((err) => {
logger.error(`[获取抽卡记录失败] ${err}`)
})
if (!res || !res.ok) {
return { retcode: 400 }
}
return await res.json()
}
/** 更新抽卡记录 */
async updateLog () {
/** 获取authkey */
let authkey = await redis.get(`${this.urlKey}${this.uid}`)
if (!authkey) return false
/** 调一次接口判断是否有效 */
let res = await this.logApi({ gacha_type: this.type, authkey, region: this.getServer() })
/** key过期或者没有数据 */
if (res.retcode !== 0 || !res?.data?.list || res.data.list.length <= 0) {
logger.debug(`${this.e.logFnc} ${res.message || 'error'}`)
return false
}
logger.mark(`${this.e.logFnc}[uid:${this.uid}] 开始获取:${this.typeName}记录...`)
let all = []
let logJson = this.readJson()
/** 第一次获取增加提示 */
if (lodash.isEmpty(logJson.list) && this.type == 301) {
await this.e.reply(`开始获取${this.typeName}记录,首次获取数据较多,请耐心等待...`)
}
let logRes = await this.getAllLog(logJson.ids, authkey)
if (logRes.hasErr) {
this.e.reply(`获取${this.typeName}记录失败`)
return false
}
/** 数据合并 */
let num = logRes.list.length
if (num > 0) {
all = logRes.list.concat(logJson.list)
/** 保存json */
this.writeJson(all)
this.all = all
}
return { num }
}
/** 递归获取所有数据 */
async getAllLog (ids, authkey, page = 1, endId = 0) {
let res = await this.logApi({
gacha_type: this.type,
page,
end_id: endId,
authkey,
region: this.getServer()
})
if (res.retcode != 0) {
return { hasErr: true, list: [] }
}
if (!res?.data?.list || res.data.list.length <= 0) {
logger.mark(`${this.e.logFnc}[uid:${this.uid}] 获取${this.typeName}记录完成,共${Number(page) - 1}`)
return { hasErr: false, list: [] }
}
let list = []
for (let val of res.data.list) {
if (ids.get(String(val.id))) {
logger.mark(`${this.e.logFnc}[uid:${this.uid}] 获取${this.typeName}记录完成,暂无新记录`)
return { hasErr: false, list }
} else {
list.push(val)
endId = val.id
}
}
page++
if (page % 3 == 0) {
await common.sleep(500)
} else {
await common.sleep(300)
}
let res2 = await this.getAllLog(ids, authkey, page, endId)
list = list.concat(res2.list)
return { hasErr: res2.hasErr, list }
}
// 读取本地json
readJson () {
let logJson = []; let ids = new Map()
let file = `${this.path}/${this.uid}/${this.type}.json`
if (fs.existsSync(file)) {
// 获取本地数据 进行数据合并
logJson = JSON.parse(fs.readFileSync(file, 'utf8'))
for (let val of logJson) {
if (val.id) {
ids.set(String(val.id), val.id)
}
}
}
return { list: logJson, ids }
}
creatFile () {
if (!fs.existsSync(this.path)) {
fs.mkdirSync(this.path)
}
if (!this.uid) return
let file = `${this.path}${this.uid}/`
if (!fs.existsSync(file)) {
fs.mkdirSync(file)
}
}
writeJson (data) {
this.creatFile()
let file = `${this.path}${this.uid}/`
fs.writeFileSync(`${file}${this.type}.json`, JSON.stringify(data, '', '\t'))
}
/** #抽卡记录 */
async getLogData () {
/** 卡池 */
this.getPool()
/** 判断uid */
await this.getUid()
if (!this.uid) {
// await this.e.reply('当前绑定uid暂无抽卡记录')
return false
}
/** 更新记录 */
if (!this.isLogUrl) await this.updateLog()
/** 统计计算记录 */
let data = this.analyse()
/** 渲染数据 */
data = this.randData(data)
return data
}
getPool () {
let msg = this.e.msg.replace(/#|抽卡|记录|祈愿|分析|池/g, '')
this.type = 301
this.typeName = '角色'
switch (msg) {
case 'up':
case '抽卡':
case '角色':
case '抽奖':
this.type = 301
this.typeName = '角色'
break
case '常驻':
this.type = 200
this.typeName = '常驻'
break
case '武器':
this.type = 302
this.typeName = '武器'
break
}
}
async getUid () {
if (!fs.existsSync(this.path)) {
this.e.reply('暂无抽卡记录\n#记录帮助,查看配置说明', false, { at: true })
return false
}
let logs = fs.readdirSync(this.path)
if (lodash.isEmpty(logs)) {
this.e.reply('暂无抽卡记录\n#记录帮助,查看配置说明', false, { at: true })
return false
}
this.uid = await redis.get(this.uidKey)
/** 记录有绑定的uid */
if (this.uid && logs.includes(String(this.uid))) {
return this.uid
}
/** 拿修改时间最后的uid */
let uidArr = []
for (let uid of logs) {
let json = `${this.path}${uid}/301.json`
if (!fs.existsSync(json)) {
continue
}
let tmp = fs.statSync(json)
uidArr.push({
uid,
mtimeMs: tmp.mtimeMs
})
}
if (uidArr.length <= 0) {
return false
}
uidArr = uidArr.sort(function (a, b) {
return b.mtimeMs - a.mtimeMs
})
this.uid = uidArr[0].uid
return uidArr[0].uid
}
/** 统计计算记录 */
analyse () {
if (lodash.isEmpty(this.all)) {
this.all = this.readJson().list
}
let fiveLog = []
let fourLog = []
let fiveNum = 0
let fourNum = 0
let fiveLogNum = 0
let fourLogNum = 0
let noFiveNum = 0
let noFourNum = 0
let wai = 0 // 歪
let weaponNum = 0
let weaponFourNum = 0
let allNum = this.all.length
let bigNum = 0
for (let val of this.all) {
this.role = val
if (val.rank_type == 4) {
fourNum++
if (noFourNum == 0) {
noFourNum = fourLogNum
}
fourLogNum = 0
if (fourLog[val.name]) {
fourLog[val.name]++
} else {
fourLog[val.name] = 1
}
if (val.item_type == '武器') {
weaponFourNum++
}
}
fourLogNum++
if (val.rank_type == 5) {
fiveNum++
if (fiveLog.length > 0) {
fiveLog[fiveLog.length - 1].num = fiveLogNum
} else {
noFiveNum = fiveLogNum
}
fiveLogNum = 0
let isUp = false
// 歪了多少个
if (val.item_type == '角色') {
if (this.checkIsUp()) {
isUp = true
} else {
wai++
}
} else {
weaponNum++
}
fiveLog.push({
name: val.name,
abbrName: gsCfg.shortName(val.name),
item_type: val.item_type,
num: 0,
isUp
})
}
fiveLogNum++
}
if (fiveLog.length > 0) {
fiveLog[fiveLog.length - 1].num = fiveLogNum
// 删除未知五星
for (let i in fiveLog) {
if (fiveLog[i].name == '未知') {
allNum = allNum - fiveLog[i].num
fiveLog.splice(i, 1)
fiveNum--
} else {
// 上一个五星是不是常驻
let lastKey = Number(i) + 1
if (fiveLog[lastKey] && !fiveLog[lastKey].isUp) {
fiveLog[i].minimum = true
bigNum++
} else {
fiveLog[i].minimum = false
}
}
}
} else {
// 没有五星
noFiveNum = allNum
}
// 四星最多
let four = []
for (let i in fourLog) {
four.push({
name: i,
num: fourLog[i]
})
}
four = four.sort((a, b) => { return b.num - a.num })
if (four.length <= 0) {
four.push({ name: '无', num: 0 })
}
let fiveAvg = 0
let fourAvg = 0
if (fiveNum > 0) {
fiveAvg = ((allNum - noFiveNum) / fiveNum).toFixed(2)
}
if (fourNum > 0) {
fourAvg = ((allNum - noFourNum) / fourNum).toFixed(2)
}
// 有效抽卡
let isvalidNum = 0
if (fiveNum > 0 && fiveNum > wai) {
if (fiveLog.length > 0 && !fiveLog[0].isUp) {
isvalidNum = (allNum - noFiveNum - fiveLog[0].num) / (fiveNum - wai)
} else {
isvalidNum = (allNum - noFiveNum) / (fiveNum - wai)
}
isvalidNum = isvalidNum.toFixed(2)
}
let upYs = isvalidNum * 160
if (upYs >= 10000) {
upYs = (upYs / 10000).toFixed(2) + 'w'
} else {
upYs = upYs.toFixed(0)
}
// 小保底不歪概率
let noWaiRate = 0
if (fiveNum > 0) {
noWaiRate = (fiveNum - bigNum - wai) / (fiveNum - bigNum)
noWaiRate = (noWaiRate * 100).toFixed(1)
}
let firstTime = this.all[this.all.length - 1].time.substring(0, 16)
let lastTime = this.all[0].time.substring(0, 16)
return {
allNum,
noFiveNum,
noFourNum,
fiveNum,
fourNum,
fiveAvg,
fourAvg,
wai,
isvalidNum,
maxFour: four[0],
weaponNum,
weaponFourNum,
firstTime,
lastTime,
fiveLog,
upYs,
noWaiRate
}
}
checkIsUp () {
if (['莫娜', '七七', '迪卢克', '琴'].includes(this.role.name)) {
return false
}
if (this.role.name == '刻晴') {
let start = new Date('2021-02-17 18:00:00').getTime()
let end = new Date('2021-03-02 15:59:59').getTime()
let logTime = new Date(this.role.time).getTime()
if (logTime < start || logTime > end) {
return false
} else {
return true
}
}
if (this.role.name == '提纳里') {
let start = new Date('2022-08-24 06:00:00').getTime()
let end = new Date('2022-09-09 17:59:59').getTime()
let logTime = new Date(this.role.time).getTime()
if (logTime < start || logTime > end) {
return false
} else {
return true
}
}
return true
}
/** 渲染数据 */
randData (data) {
let line = []
if (this.type == 301) {
line = [[
{ lable: '未出五星', num: data.noFiveNum, unit: '抽' },
{ lable: '五星', num: data.fiveNum, unit: '个' },
{ lable: '五星平均', num: data.fiveAvg, unit: '抽', color: data.fiveColor },
{ lable: '小保底不歪', num: data.noWaiRate + '%', unit: '' }
], [
{ lable: '未出四星', num: data.noFourNum, unit: '抽' },
{ lable: '五星常驻', num: data.wai, unit: '个' },
{ lable: 'UP平均', num: data.isvalidNum, unit: '抽' },
{ lable: 'UP花费原石', num: data.upYs, unit: '' }
]]
}
// 常驻池
if (this.type == 200) {
line = [[
{ lable: '未出五星', num: data.noFiveNum, unit: '抽' },
{ lable: '五星', num: data.fiveNum, unit: '个' },
{ lable: '五星平均', num: data.fiveAvg, unit: '抽', color: data.fiveColor },
{ lable: '五星武器', num: data.weaponNum, unit: '个' }
], [
{ lable: '未出四星', num: data.noFourNum, unit: '抽' },
{ lable: '四星', num: data.fourNum, unit: '个' },
{ lable: '四星平均', num: data.fourAvg, unit: '抽' },
{ lable: '四星最多', num: data.maxFour.num, unit: data.maxFour.name }
]]
}
// 武器池
if (this.type == 302) {
line = [[
{ lable: '未出五星', num: data.noFiveNum, unit: '抽' },
{ lable: '五星', num: data.fiveNum, unit: '个' },
{ lable: '五星平均', num: data.fiveAvg, unit: '抽', color: data.fiveColor },
{ lable: '四星武器', num: data.weaponFourNum, unit: '个' }
], [
{ lable: '未出四星', num: data.noFourNum, unit: '抽' },
{ lable: '四星', num: data.fourNum, unit: '个' },
{ lable: '四星平均', num: data.fourAvg, unit: '抽' },
{ lable: '四星最多', num: data.maxFour.num, unit: data.maxFour.name }
]]
}
let hasMore = false
if (this.e.isGroup && data.fiveLog.length > 48) {
data.fiveLog = data.fiveLog.slice(0, 48)
hasMore = true
}
return {
...this.screenData,
saveId: this.uid,
uid: this.uid,
type: this.type,
typeName: this.typeName,
allNum: data.allNum,
firstTime: data.firstTime,
lastTime: data.lastTime,
fiveLog: data.fiveLog,
line,
hasMore
}
}
getServer () {
let uid = this.uid
switch (String(uid)[0]) {
case '1':
case '2':
return 'cn_gf01' // 官服
case '5':
return 'cn_qd01' // B服
case '6':
return 'os_usa' // 美服
case '7':
return 'os_euro' // 欧服
case '8':
return 'os_asia' // 亚服
case '9':
return 'os_cht' // 港澳台服
}
return 'cn_gf01'
}
}