diff --git a/apps/abyss.js b/apps/abyss.js index 9b030b3..6dfb0ec 100644 --- a/apps/abyss.js +++ b/apps/abyss.js @@ -25,7 +25,10 @@ export class Abyss extends ZZZPlugin { const method = this.e.msg.match(`(上期|往期)`) ? 'zzzChallengePeriod' : 'zzzChallenge'; - const abyssData = await api.getFinalData(this.e, method); + const abyssData = await api.getFinalData(method).catch(e => { + this.reply(e.message); + throw e; + }); if (!abyssData?.has_data) { await this.reply('没有式舆防卫战数据'); return false; diff --git a/apps/card.js b/apps/card.js index 7c4f025..7f84cf4 100644 --- a/apps/card.js +++ b/apps/card.js @@ -22,14 +22,23 @@ export class Card extends ZZZPlugin { async card() { const { api } = await this.getAPI(); await this.getPlayerInfo(); - const indexData = await api.getFinalData(this.e, 'zzzIndex'); + const indexData = await api.getFinalData('zzzIndex').catch(e => { + this.reply(e.message); + throw e; + }); if (!indexData) return false; - let zzzAvatarList = await api.getFinalData(this.e, 'zzzAvatarList'); + let zzzAvatarList = await api.getFinalData('zzzAvatarList').catch(e => { + this.reply(e.message); + throw e; + }); if (!zzzAvatarList) return false; indexData.avatar_list = zzzAvatarList.avatar_list; - let zzzBuddyList = await api.getFinalData(this.e, 'zzzBuddyList'); + let zzzBuddyList = await api.getFinalData('zzzBuddyList').catch(e => { + this.reply(e.message); + throw e; + }); if (!zzzBuddyList) return false; indexData.buddy_list = zzzBuddyList.list; const finalIndexData = new ZZZIndexResp(indexData); diff --git a/apps/gachalog.js b/apps/gachalog.js index 630aea4..584aa50 100644 --- a/apps/gachalog.js +++ b/apps/gachalog.js @@ -3,13 +3,9 @@ import { getAuthKey } from '../lib/authkey.js'; import settings from '../lib/settings.js'; import _ from 'lodash'; import common from '../../../lib/common/common.js'; -import { - anaylizeGachaLog, - updateGachaLog, - getZZZGachaLink, - gacha_type_meta_data, - getZZZGachaLogByAuthkey, -} from '../lib/gacha.js'; +import { anaylizeGachaLog, updateGachaLog } from '../lib/gacha.js'; +import { getZZZGachaLink, getZZZGachaLogByAuthkey } from '../lib/gacha/core.js'; +import { gacha_type_meta_data } from '../lib/gacha/const.js'; import { getQueryVariable } from '../utils/network.js'; import { rulePrefix } from '../lib/common.js'; diff --git a/apps/guide.js b/apps/guide.js index 37be51d..04abbe7 100644 --- a/apps/guide.js +++ b/apps/guide.js @@ -7,7 +7,7 @@ import { ZZZPlugin } from '../lib/plugin.js'; import { imageResourcesPath } from '../lib/path.js'; import _ from 'lodash'; import settings from '../lib/settings.js'; -import { downloadFile } from '../lib/download.js'; +import { downloadFile } from '../lib/download/core.js'; import { char } from '../lib/convert.js'; import guides from '../lib/guides.js'; import { rulePrefix } from '../lib/common.js'; diff --git a/apps/help.js b/apps/help.js index 74df78c..cb53702 100644 --- a/apps/help.js +++ b/apps/help.js @@ -251,7 +251,7 @@ export class Help extends ZZZPlugin { }, { title: '删除资源(需注意)', - desc: '请注意,此命令会删除自定义面板图,请确认做好备份后再执行!!!删除已经下载的资源,查询时需要再次下载(用于删除错误下载缓存)。', + desc: '删除已经下载的资源,查询时需要再次下载(用于删除错误下载缓存)。', needCK: false, needSK: false, commands: ['删除全部/所有资源'], diff --git a/apps/manage/assets.js b/apps/manage/assets.js index 4cf758f..24e3333 100644 --- a/apps/manage/assets.js +++ b/apps/manage/assets.js @@ -9,7 +9,7 @@ import { import { char } from '../../lib/convert.js'; import { getAllEquipID } from '../../lib/convert/equip.js'; import { getAllWeaponID } from '../../lib/convert/weapon.js'; -import { imageResourcesPath } from '../../lib/path.js'; +import * as LocalURI from '../../lib/download/const.js'; export async function downloadAll() { if (!this.e.isMaster) return false; @@ -91,34 +91,11 @@ export async function downloadAll() { } const messages = [ '资源下载完成(成功的包含先前下载的图片)', - '角色图需下载' + - charIDs.length + - '张,成功' + - result.char.success + - '张,失败' + - result.char.failed + - '张', - '角色头像图需下载' + - charIDs.length + - '张,成功' + - result.charSquare.success + - '张,失败' + - result.charSquare.failed + - '张', - '套装图需下载' + - equipSprites.length + - '张,成功' + - result.equip.success + - '张,失败' + - result.equip.failed + - '张', - '武器图需下载' + - weaponSprites.length + - '张,成功' + - result.weapon.success + - '张,失败' + - result.weapon.failed + - '张', + `角色图需下载${charIDs.length}张,成功${result.char.success}张,失败${result.char.failed}张`, + `角色头像图需下载${charIDs.length}张,成功${esult.charSquare.success}张,失败${result.charSquare.failed}张`, + `角色头像图(练度统计)需下载${charIDs.length}张,成功${result.charSmallSquare.success}张,失败${result.charSmallSquare.failed}张`, + `驱动盘套装图需下载${equipSprites.length}张,成功${result.equip.success}张,失败${result.equip.failed}张`, + `武器图需下载${weaponSprites.length}张,成功${result.weapon.success}张,失败${result.weapon.failed}张`, ]; await this.reply(messages.join('\n')); } @@ -129,8 +106,10 @@ export async function deleteAll() { false, { at: true, recallMsg: 100 } ); - if (fs.existsSync(imageResourcesPath)) { - fs.rmSync(imageResourcesPath, { recursive: true }); + for (const dir of LocalURI) { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true }); + } } await this.reply('资源图片已删除!', false, { at: true, recallMsg: 100 }); } diff --git a/apps/manage/panel.js b/apps/manage/panel.js index e24f949..dcc45f3 100644 --- a/apps/manage/panel.js +++ b/apps/manage/panel.js @@ -1,5 +1,5 @@ import { char } from '../../lib/convert.js'; -import { downloadFile } from '../../lib/download.js'; +import { downloadFile } from '../../lib/download/core.js'; import { imageResourcesPath } from '../../lib/path.js'; import common from '../../../../lib/common/common.js'; import fs from 'fs'; diff --git a/apps/note.js b/apps/note.js index 87c994a..e40620f 100644 --- a/apps/note.js +++ b/apps/note.js @@ -22,7 +22,10 @@ export class Note extends ZZZPlugin { async note() { const { api } = await this.getAPI(); await this.getPlayerInfo(); - const noteResponse = await api.getFinalData(this.e, 'zzzNote'); + const noteResponse = await api.getFinalData('zzzNote').catch(e => { + this.reply(e.message); + throw e; + }); if (!noteResponse) return false; const noteData = new ZZZNoteResp(noteResponse); const finalData = { diff --git a/apps/panel.js b/apps/panel.js index 00f6e57..d4a357d 100644 --- a/apps/panel.js +++ b/apps/panel.js @@ -53,7 +53,10 @@ export class Panel extends ZZZPlugin { await redis.set(`ZZZ:PANEL:${uid}:LASTTIME`, Date.now()); await this.reply('正在刷新面板列表,请稍候...'); await this.getPlayerInfo(); - const result = await refreshPanel(this.e, api, uid); + const result = await refreshPanel(api, uid).catch(e => { + this.reply(e.message); + throw e; + }); if (!result) { await this.reply('面板列表刷新失败,请稍后再试'); return false; diff --git a/lib/assets.js b/lib/assets.js index b9eeeee..8d823e3 100644 --- a/lib/assets.js +++ b/lib/assets.js @@ -1,41 +1,16 @@ import { findLowestLatencyUrl } from '../utils/network.js'; - +import { + URL_LIB, + TYPE_PATH, + RESOURCE_PATH, + GUIDE_PATH, +} from './assets/const.js'; // 保存上次找的节点 let lastFindFastestUrl = { url: null, time: 0, }; -// 节点列表 -const URL_LIB = { - '[JPFRP]': 'http://jp-3.lcf.1l1.icu:17217', - '[HKFRP]': 'http://hk-1.lcf.1l1.icu:10200', - '[USFRP]': 'http://us-6.lcf.1l1.icu:28596', - '[XiaoWu]': 'http://frp.xiaowuap.com:63481', - '[Chuncheon]': 'https://kr.qxqx.cf', - '[Seoul]': 'https://kr-s.qxqx.cf', - '[Singapore]': 'https://sg.qxqx.cf', -}; - -// 文件类型路径 -const TYPE_PATH = { - wiki: 'wiki', - resource: 'resource', - guide: 'guide', -}; - -// 资源路径 -const RESOURCE_PATH = { - role: 'role', - role_circle: 'role_circle', - weapon: 'weapon', -}; - -// 图鉴路径 -const GUIDE_PATH = { - flower: 'flower', -}; - /** * 获取最快节点 * @returns {Promise} @@ -61,10 +36,10 @@ export const getFatestUrl = async () => { }; /** - * Get resource remote path - * @param {keyof TYPE_PATH} type - * @param {keyof RESOURCE_PATH | keyof GUIDE_PATH} label - * @param {string} name + * 获取远程路径 + * @param {keyof TYPE_PATH} type 资源类型 + * @param {keyof RESOURCE_PATH | keyof GUIDE_PATH} label 资源标签 + * @param {string} name 资源名称 * @returns */ export const getRemotePath = async (type, label, name) => { @@ -72,7 +47,12 @@ export const getRemotePath = async (type, label, name) => { return `${url}/ZZZeroUID/${type}/${label}/${name}`; }; -// 获取资源远程路径 +/** + * 获取资源远程路径 + * @param {keyof RESOURCE_PATH} label 资源标签 + * @param {string} name 资源名称 + * @returns {Promise} + */ export const getResourceRemotePath = async (label, name) => { return getRemotePath(TYPE_PATH.resource, label, name); }; diff --git a/lib/assets/const.js b/lib/assets/const.js new file mode 100644 index 0000000..dfe10f1 --- /dev/null +++ b/lib/assets/const.js @@ -0,0 +1,29 @@ +// 节点列表 +export const URL_LIB = { + '[JPFRP]': 'http://jp-3.lcf.1l1.icu:17217', + '[HKFRP]': 'http://hk-1.lcf.1l1.icu:10200', + '[USFRP]': 'http://us-6.lcf.1l1.icu:28596', + '[XiaoWu]': 'http://frp.xiaowuap.com:63481', + '[Chuncheon]': 'https://kr.qxqx.cf', + '[Seoul]': 'https://kr-s.qxqx.cf', + '[Singapore]': 'https://sg.qxqx.cf', +}; + +// 文件类型路径 +export const TYPE_PATH = { + wiki: 'wiki', + resource: 'resource', + guide: 'guide', +}; + +// 资源路径 +export const RESOURCE_PATH = { + role: 'role', + role_circle: 'role_circle', + weapon: 'weapon', +}; + +// 图鉴路径 +export const GUIDE_PATH = { + flower: 'flower', +}; diff --git a/lib/avatar.js b/lib/avatar.js index 54c2cd5..b131b4b 100644 --- a/lib/avatar.js +++ b/lib/avatar.js @@ -5,14 +5,13 @@ import { char } from './convert.js'; /** * 获取角色基础信息列表 - * @param {*} e 消息事件 * @param {MysZZZApi} api * @param {boolean} origin 是否返回原始数据 * @returns {Promise} */ -export const getAvatarBasicList = async (e, api, origin = false) => { +export const getAvatarBasicList = async (api, origin = false) => { // 获取米游社角色列表 - const avatarBaseListData = await api.getFinalData(e, 'zzzAvatarList'); + const avatarBaseListData = await api.getFinalData('zzzAvatarList'); if (!avatarBaseListData) return null; // 是否返回原始数据 if (origin) return avatarBaseListData.avatar_list; @@ -25,17 +24,16 @@ export const getAvatarBasicList = async (e, api, origin = false) => { /** * 获取角色详细信息列表 - * @param {*} e 消息事件 * @param {MysZZZApi} api * @returns {Promise} * @param {boolean} origin 是否返回原始数据 */ -export const getAvatarInfoList = async (e, api, origin = false) => { +export const getAvatarInfoList = async (api, origin = false) => { // 获取角色基础信息列表 - const avatarBaseList = await getAvatarBasicList(e, api, origin); + const avatarBaseList = await getAvatarBasicList(api, origin); if (!avatarBaseList) return null; // 获取角色详细信息 - const avatarInfoList = await api.getFinalData(e, 'zzzAvatarInfo', { + const avatarInfoList = await api.getFinalData('zzzAvatarInfo', { query: { id_list: avatarBaseList.map(item => item.id), }, @@ -52,16 +50,15 @@ export const getAvatarInfoList = async (e, api, origin = false) => { /** * 刷新面板 - * @param {*} e 消息事件 * @param {MysZZZApi} api * @param {string} uid * @returns {Promise} */ -export const refreshPanel = async (e, api, uid) => { +export const refreshPanel = async (api, uid) => { // 获取已保存数据 const originData = getPanelData(uid); // 获取新数据 - const newData = await getAvatarInfoList(e, api, true); + const newData = await getAvatarInfoList(api, true); if (!newData) return null; // 初始化最终数据 const finalData = [...newData]; diff --git a/lib/common.js b/lib/common.js index cbf6804..7da0a1f 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,7 +1,7 @@ import User from '../../genshin/model/user.js'; import { getStoken } from './authkey.js'; -export const rulePrefix = '^((#|%)?(zzz|ZZZ|绝区零))'; +export const rulePrefix = '^((#|%|/)?(zzz|ZZZ|绝区零))'; /** * 获取米游社用户的 cookie diff --git a/lib/db.js b/lib/db.js index 3a79451..9e76214 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,47 +1,4 @@ -import { readFileSync, writeFileSync } from 'fs'; -import path from 'path'; -import { checkFolderExistAndCreate } from '../utils/file.js'; -import { dataPath } from './path.js'; -const dbPath = { - gacha: 'gacha', - panel: 'panel', -}; - -/** - * - * @param {string} dbName - * @param {string} dbFile - * @returns {object | Array | null} - */ -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.debug(`读取数据库失败: ${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.debug(`读取数据库失败: ${error.message}`); - } -} +import { getDB, setDB } from './db/core.js'; /** * @param {string} uid diff --git a/lib/db/core.js b/lib/db/core.js new file mode 100644 index 0000000..1ee5b93 --- /dev/null +++ b/lib/db/core.js @@ -0,0 +1,44 @@ +import { readFileSync, writeFileSync } from 'fs'; +import path from 'path'; +import { checkFolderExistAndCreate } from '../../utils/file.js'; +import { dataPath } from '../path.js'; +export const dbPath = { + gacha: 'gacha', + panel: 'panel', +}; + +/** + * 读取数据库 + * @param {keyof dbPath} dbName + * @param {string} dbFile + * @returns {object | Array | null} + */ +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.debug(`读取数据库失败: ${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.debug(`读取数据库失败: ${error.message}`); + } +} diff --git a/lib/download.js b/lib/download.js index a99ebc4..d3b0b57 100644 --- a/lib/download.js +++ b/lib/download.js @@ -1,72 +1,8 @@ -import path from 'path'; -import fs from 'fs'; +import * as convert from './convert.js'; import { - ZZZ_SQUARE_AVATAR, - ZZZ_SQUARE_BANGBOO, - NEW_ZZZ_SQUARE_BANGBOO, - NEW_ZZZ_SQUARE_AVATAR, -} from './mysapi/api.js'; -import { imageResourcesPath } from './path.js'; -import { char, equip, weapon } from './convert.js'; -import { getResourceRemotePath } from './assets.js'; -import request from '../utils/request.js'; - -const ZZZ_SQUARE_AVATAR_PATH = path.join(imageResourcesPath, 'square_avatar'); -const ZZZ_SMALL_SQUARE_AVATAR_PATH = path.join( - imageResourcesPath, - 'role_general' -); -const ZZZ_SQUARE_BANGBOO_PATH = path.join( - imageResourcesPath, - 'bangboo_square_avatar' -); -const ZZZ_WEAPON_PATH = path.join(imageResourcesPath, 'weapon'); -const ZZZ_ROLE_PATH = path.join(imageResourcesPath, 'role'); -const ZZZ_ROLE_CIRCLE_PATH = path.join(imageResourcesPath, 'role_circle'); -const ZZZ_SUIT_3D_PATH = path.join(imageResourcesPath, 'suit_3d'); -const ZZZ_SUIT_PATH = path.join(imageResourcesPath, 'suit'); -// const ZZZ_GUIDES_PATH = path.join(imageResourcesPath, 'guides'); - -/** - * 下载文件 - * @param {string} url 下载地址 - * @param {string} savePath 保存路径 - * @returns {Promise} 保存路径 - */ -export const downloadFile = async (url, savePath) => { - // 下载文件 - try { - const download = await request(url, {}, 5); - const arrayBuffer = await download.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - // 保存文件 - if (!fs.existsSync(path.dirname(savePath))) { - fs.mkdirSync(path.dirname(savePath), { recursive: true }); - } - fs.writeFileSync(savePath, buffer); - // 返回保存路径 - return savePath; - } catch (error) { - return null; - } -}; - -/** - * 查看文件是否存在,如果存在则返回路径,否则下载文件 - * @param {string} url 下载地址 - * @param {string} savePath 保存路径 - * @returns {Promise} 保存路径 - */ -export const checkFile = async (url, savePath) => { - if (fs.existsSync(savePath)) { - const stats = fs.statSync(savePath); - if (stats.size > 0) { - return savePath; - } - } - const download = await downloadFile(url, savePath); - return download; -}; + downloadMysImage, + downloadResourceImage, +} from './download/download.js'; /** * 获取角色头像(方形) @@ -75,96 +11,94 @@ export const checkFile = async (url, savePath) => { */ export const getSquareAvatar = async charID => { const filename = `role_square_avatar_${charID}.png`; - const avatarPath = path.join(ZZZ_SQUARE_AVATAR_PATH, filename); - let url = `${ZZZ_SQUARE_AVATAR}/${filename}`; - let result = await checkFile(url, avatarPath); - if (!result) { - url = `${NEW_ZZZ_SQUARE_AVATAR}/${filename}`; - result = await checkFile(url, avatarPath); - } + const result = await downloadMysImage( + 'ZZZ_SQUARE_AVATAR', + 'ZZZ_SQUARE_AVATAR_PATH', + filename, + 'NEW_ZZZ_SQUARE_AVATAR' + ); return result; }; /** * 获取角色头像(小方形) * @param {string | number} charID - * @returns Promise * @returns {Promise} */ export const getSmallSquareAvatar = async charID => { - const sprite = char.IDToCharSprite(charID); + const sprite = convert.char.IDToCharSprite(charID); if (!sprite) return null; const filename = `IconRoleGeneral${sprite}.png`; - const avatarPath = path.join(ZZZ_SMALL_SQUARE_AVATAR_PATH, filename); - const url = await getResourceRemotePath('role_general', filename); - const result = await checkFile(url, avatarPath); + const result = await downloadResourceImage( + 'role_general', + 'ZZZ_SMALL_SQUARE_AVATAR_PATH', + filename + ); return result; }; /** * 获取邦布头像(方形) * @param {string | number} bangbooId - * @returns Promise + * @returns {Promise} */ export const getSquareBangboo = async bangbooId => { const filename = `bangboo_rectangle_avatar_${bangbooId}.png`; - const bangbooPath = path.join(ZZZ_SQUARE_BANGBOO_PATH, filename); - let url = `${ZZZ_SQUARE_BANGBOO}/${filename}`; - let result = await checkFile(url, bangbooPath); - if (!result) { - url = `${NEW_ZZZ_SQUARE_BANGBOO}/${filename}`; - result = await checkFile(url, bangbooPath); - } + const result = await downloadMysImage( + 'ZZZ_SQUARE_BANGBOO', + 'ZZZ_SQUARE_BANGBOO_PATH', + filename, + 'NEW_ZZZ_SQUARE_BANGBOO' + ); return result; }; /** * 获取武器图片 * @param {string} id - * @returns Promise + * @returns {Promise} */ export const getWeaponImage = async id => { - const name = weapon.IDToWeaponFileName(id); - let filename = `${name}_High.png`; - const weaponPath = path.join(ZZZ_WEAPON_PATH, filename); - const url = await getResourceRemotePath('weapon', filename); - let result = await checkFile(url, weaponPath); - if (!result) { - filename = `${name}.png`; - const weaponPath = path.join(ZZZ_WEAPON_PATH, filename); - const url = await getResourceRemotePath('weapon', filename); - result = await checkFile(url, weaponPath); - } + const name = convert.weapon.IDToWeaponFileName(id); + if (!name) return null; + const filename = `${name}_High.png`; + const replaceFilename = `${name}_High.png`; + const result = await downloadResourceImage( + 'weapon', + 'ZZZ_WEAPON_PATH', + filename, + replaceFilename + ); return result; }; /** * 获取角色图片 * @param {string | number} id - * @returns Promise + * @returns {Promise} */ export const getRoleImage = async id => { - const sprite = char.IDToCharSprite(id); + const sprite = convert.char.IDToCharSprite(id); if (!sprite) return null; const filename = `IconRole${sprite}.png`; - const rolePath = path.join(ZZZ_ROLE_PATH, filename); - const url = await getResourceRemotePath('role', filename); - const result = await checkFile(url, rolePath); + const result = await downloadResourceImage('role', 'ZZZ_ROLE_PATH', filename); return result; }; /** * 获取角色圆形图片 * @param {string | number} id - * @returns Promise + * @returns {Promise} */ export const getRoleCircleImage = async id => { - const sprite = char.IDToCharSprite(id); + const sprite = convert.char.IDToCharSprite(id); if (!sprite) return null; const filename = `IconRoleCircle${sprite}.png`; - const roleCirclePath = path.join(ZZZ_ROLE_CIRCLE_PATH, filename); - const url = await getResourceRemotePath('role_circle', filename); - const result = await checkFile(url, roleCirclePath); + const result = await downloadResourceImage( + 'role_circle', + 'ZZZ_ROLE_CIRCLE_PATH', + filename + ); return result; }; @@ -174,11 +108,10 @@ export const getRoleCircleImage = async id => { * @returns Promise */ export const getSuitImage = async suitId => { - const suitName = equip.equipIdToSprite(suitId); + const suitName = convert.equip.equipIdToSprite(suitId); + if (!suitName) return null; const filename = `${suitName}.png`; - const suitPath = path.join(ZZZ_SUIT_PATH, filename); - const url = await getResourceRemotePath('suit', filename); - const result = await checkFile(url, suitPath); + const result = await downloadResourceImage('suit', 'ZZZ_SUIT_PATH', filename); return result; }; @@ -188,10 +121,12 @@ export const getSuitImage = async suitId => { * @returns Promise */ export const getSuit3DImage = async suitId => { - const suitName = equip.equipIdToSprite(suitId); + const suitName = convert.equip.equipIdToSprite(suitId); const filename = `${suitName}_3d.png`; - const suitPath = path.join(ZZZ_SUIT_3D_PATH, filename); - const url = await getResourceRemotePath('suit_3d', filename); - const result = await checkFile(url, suitPath); + const result = await downloadResourceImage( + 'suit_3d', + 'ZZZ_SUIT_3D_PATH', + filename + ); return result; }; diff --git a/lib/download/const.js b/lib/download/const.js new file mode 100644 index 0000000..d653873 --- /dev/null +++ b/lib/download/const.js @@ -0,0 +1,18 @@ +import path from 'path'; +import { imageResourcesPath } from '../path.js'; + +export const ZZZ_SQUARE_AVATAR_PATH = path.join( + imageResourcesPath, + 'square_avatar' + ), + ZZZ_SMALL_SQUARE_AVATAR_PATH = path.join(imageResourcesPath, 'role_general'), + ZZZ_SQUARE_BANGBOO_PATH = path.join( + imageResourcesPath, + 'bangboo_square_avatar' + ), + ZZZ_WEAPON_PATH = path.join(imageResourcesPath, 'weapon'), + ZZZ_ROLE_PATH = path.join(imageResourcesPath, 'role'), + ZZZ_ROLE_CIRCLE_PATH = path.join(imageResourcesPath, 'role_circle'), + ZZZ_SUIT_3D_PATH = path.join(imageResourcesPath, 'suit_3d'), + ZZZ_SUIT_PATH = path.join(imageResourcesPath, 'suit'); +// const ZZZ_GUIDES_PATH = path.join(imageResourcesPath, 'guides'); diff --git a/lib/download/core.js b/lib/download/core.js new file mode 100644 index 0000000..64f4f93 --- /dev/null +++ b/lib/download/core.js @@ -0,0 +1,44 @@ +import path from 'path'; +import fs from 'fs'; +import request from '../../utils/request.js'; + +/** + * 下载文件 + * @param {string} url 下载地址 + * @param {string} savePath 保存路径 + * @returns {Promise} 保存路径 + */ +export const downloadFile = async (url, savePath) => { + // 下载文件 + try { + const download = await request(url, {}, 5); + const arrayBuffer = await download.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + // 保存文件 + if (!fs.existsSync(path.dirname(savePath))) { + fs.mkdirSync(path.dirname(savePath), { recursive: true }); + } + fs.writeFileSync(savePath, buffer); + // 返回保存路径 + return savePath; + } catch (error) { + return null; + } +}; + +/** + * 查看文件是否存在,如果存在则返回路径,否则下载文件 + * @param {string} url 下载地址 + * @param {string} savePath 保存路径 + * @returns {Promise} 保存路径 + */ +export const checkFile = async (url, savePath) => { + if (fs.existsSync(savePath)) { + const stats = fs.statSync(savePath); + if (stats.size > 0) { + return savePath; + } + } + const download = await downloadFile(url, savePath); + return download; +}; diff --git a/lib/download/download.js b/lib/download/download.js new file mode 100644 index 0000000..942943b --- /dev/null +++ b/lib/download/download.js @@ -0,0 +1,57 @@ +import path from 'path'; +import { checkFile } from './core.js'; +import { getResourceRemotePath } from '../assets.js'; +import * as MysURL from '../mysapi/api.js'; +import * as LocalURI from './const.js'; +/** + * 下载米游社图片 + * @param {keyof MysURL} base 远程地址 + * @param {keyof LocalURI} localBase 本地地址 + * @param {string} filename 文件名 + * @param {keyof MysURL} newBase 新远程地址 + */ +export const downloadMysImage = async ( + base, + localBase, + filename, + newBase = '' +) => { + base = MysURL[base]; + localBase = LocalURI[localBase]; + if (!!newBase) { + newBase = MysURL[newBase]; + } + const finalPath = path.join(localBase, filename); + let url = `${base}/${filename}`; + let result = await checkFile(url, finalPath); + if (!result && !!newBase) { + url = `${newBase}/${filename}`; + result = await checkFile(url, finalPath); + } + return result; +}; + +/** + * 下载资源库图片 + * @param {Parameters[0]} remoteLabel 远程地址 + * @param {keyof LocalURI} localBase 本地地址 + * @param {string} filename 文件名 + * @param {string} replaceFilename 替换文件名(如果资源不存在) + */ +export const downloadResourceImage = async ( + remoteLabel, + localBase, + filename, + replaceFilename = '' +) => { + localBase = LocalURI[localBase]; + const finalPath = path.join(localBase, filename); + const url = await getResourceRemotePath(remoteLabel, filename); + let result = await checkFile(url, finalPath); + if (!result && !!replaceFilename) { + const finalPath = path.join(localBase, replaceFilename); + const url = await getResourceRemotePath(remoteLabel, replaceFilename); + result = await checkFile(url, finalPath); + } + return result; +}; diff --git a/lib/error.js b/lib/error.js new file mode 100644 index 0000000..ca4e72a --- /dev/null +++ b/lib/error.js @@ -0,0 +1,37 @@ +export class MysError extends Error { + /** + * 自定义错误类 + * @param {string} code 错误码 + */ + constructor(code, uid, result) { + let msg = '未知错误'; + switch (code) { + case '10102': + if (result.message === 'Data is not public for the user') { + msg = `UID:${uid},米游社数据未公开`; + } else { + msg = `UID:${uid},请先去米游社绑定角色`; + } + break; + case '10041': + case '5003': + msg = `UID:${uid},米游社账号异常,暂时无法查询,请发送 %绑定设备帮助 查看如何绑定设备`; + break; + case '10035': + case '1034': + msg = `UID:${uid},米游社查询遇到验证码`; + break; + default: + if (/(登录|login)/i.test(res.message)) { + msg = `UID:${uid},米游社cookie已失效`; + } else { + msg = `UID:${uid},米游社接口报错:${res.message}`; + } + } + super(msg); + this.code = Number(code); + this.uid = uid; + this.result = result; + this.name = 'MysError'; + } +} diff --git a/lib/gacha.js b/lib/gacha.js index 01b174e..4d689f4 100644 --- a/lib/gacha.js +++ b/lib/gacha.js @@ -1,139 +1,16 @@ -import { SingleGachaLog, ZZZGachaLogResp } from '../model/gacha.js'; +import { SingleGachaLog } from '../model/gacha.js'; import { sleep } from '../utils/time.js'; import { rank } from './convert.js'; import { getGachaLog, saveGachaLog } from './db.js'; -import { ZZZ_GET_GACHA_LOG_API } from './mysapi/api.js'; -import request from '../utils/request.js'; - -// 池子代码 -export const gacha_type_meta_data = { - 音擎频段: ['3001'], - 独家频段: ['2001'], - 常驻频段: ['1001'], - 邦布频段: ['5001'], -}; - -// 欧非阈值 -const FLOORS_MAP = { - 邦布频段: [50, 70], - 音擎频段: [50, 70], - 独家频段: [60, 80], - 常驻频段: [60, 80], -}; - -// 欧非标签 -const HOMO_TAG = ['非到极致', '运气不好', '平稳保底', '小欧一把', '欧狗在此']; - -// 欧非表情 -const EMOJI = [ - [4, 8, 13], - [1, 10, 5], - [16, 15, 2], - [12, 3, 9], - [6, 14, 7], -]; - -// 常驻名称 -const NORMAL_LIST = [ - '「11号」', - '猫又', - '莱卡恩', - '丽娜', - '格莉丝', - '珂蕾妲', - '拘缚者', - '燃狱齿轮', - '嵌合编译器', - '钢铁肉垫', - '硫磺石', - '啜泣摇篮', -]; - -/** - * 获取抽卡链接 - * @param {string} authKey 米游社认证密钥 - * @param {string} gachaType 祈愿类型(池子代码) - * @param {string} initLogGachaBaseType - * @param {number} page 页数 - * @param {string} endId 最后一个数据的 id - * @param {string} size 页面数据大小 - * @returns {Promise} 抽卡链接 - */ -export const getZZZGachaLink = async ( - authKey, - gachaType = '2001', - initLogGachaBaseType = '2', - page = 1, - endId = '0', - size = '20' -) => { - // 暂时直接写死服务器为国服 - 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: size, - end_id: endId, - }); - // 完整链接 - return `${url}?${params}`; -}; - -/** - * 通过米游社认证密钥获取抽卡记录 - * @param {string} authKey 米游社认证密钥 - * @param {string} gachaType 祈愿类型(池子代码) - * @param {string} initLogGachaBaseType - * @param {number} page 页数 - * @param {string} endId 最后一个数据的 id - * @returns {Promise} 抽卡记录 - */ -export const getZZZGachaLogByAuthkey = async ( - authKey, - gachaType = '2001', - initLogGachaBaseType = '2', - page = 1, - endId = '0' -) => { - // 获取抽卡链接 - const link = await getZZZGachaLink( - authKey, - gachaType, - initLogGachaBaseType, - page, - endId - ); - // 发送请求 - const response = await request(link, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - retry: 3, - }); - // 获取数据 - const data = await response.json(); - if (!data || !data?.data) return null; - - return new ZZZGachaLogResp(data.data); -}; +import { + gacha_type_meta_data, + FLOORS_MAP, + HOMO_TAG, + EMOJI, + NORMAL_LIST, +} from './gacha/const.js'; +import { getLevelFromList } from './gacha/tool.js'; +import { getZZZGachaLogByAuthkey } from './gacha/core.js'; /** * 更新抽卡数据 @@ -219,26 +96,6 @@ export const updateGachaLog = async (authKey, uid) => { }; }; -/** - * 欧非分析 - * @param {number} ast - * @param {number[]} lst - */ -const getLevelFromList = (ast, lst) => { - if (ast === 0) { - return 2; - } - - let level = 0; - for (let numIndex = 0; numIndex < lst.length; numIndex++) { - if (ast <= lst[numIndex]) { - level = 4 - numIndex; - break; - } - } - return level; -}; - /** * 抽卡分析 * @param {string} uid ZZZUID diff --git a/lib/gacha/const.js b/lib/gacha/const.js new file mode 100644 index 0000000..403797a --- /dev/null +++ b/lib/gacha/const.js @@ -0,0 +1,49 @@ +// 池子代码 +export const gacha_type_meta_data = { + 音擎频段: ['3001'], + 独家频段: ['2001'], + 常驻频段: ['1001'], + 邦布频段: ['5001'], +}; + +// 欧非阈值 +export const FLOORS_MAP = { + 邦布频段: [50, 70], + 音擎频段: [50, 70], + 独家频段: [60, 80], + 常驻频段: [60, 80], +}; + +// 欧非标签 +export const HOMO_TAG = [ + '非到极致', + '运气不好', + '平稳保底', + '小欧一把', + '欧狗在此', +]; + +// 欧非表情 +export const EMOJI = [ + [4, 8, 13], + [1, 10, 5], + [16, 15, 2], + [12, 3, 9], + [6, 14, 7], +]; + +// 常驻名称 +export const NORMAL_LIST = [ + '「11号」', + '猫又', + '莱卡恩', + '丽娜', + '格莉丝', + '珂蕾妲', + '拘缚者', + '燃狱齿轮', + '嵌合编译器', + '钢铁肉垫', + '硫磺石', + '啜泣摇篮', +]; diff --git a/lib/gacha/core.js b/lib/gacha/core.js new file mode 100644 index 0000000..523a047 --- /dev/null +++ b/lib/gacha/core.js @@ -0,0 +1,89 @@ +import { ZZZ_GET_GACHA_LOG_API } from '../mysapi/api.js'; +import request from '../../utils/request.js'; +import { ZZZGachaLogResp } from '../../model/gacha.js'; + +/** + * 获取抽卡链接 + * @param {string} authKey 米游社认证密钥 + * @param {string} gachaType 祈愿类型(池子代码) + * @param {string} initLogGachaBaseType + * @param {number} page 页数 + * @param {string} endId 最后一个数据的 id + * @param {string} size 页面数据大小 + * @returns {Promise} 抽卡链接 + */ +export const getZZZGachaLink = async ( + authKey, + gachaType = '2001', + initLogGachaBaseType = '2', + page = 1, + endId = '0', + size = '20' +) => { + // 暂时直接写死服务器为国服 + 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: size, + end_id: endId, + }); + // 完整链接 + return `${url}?${params}`; +}; + +/** + * 通过米游社认证密钥获取抽卡记录 + * @param {string} authKey 米游社认证密钥 + * @param {string} gachaType 祈愿类型(池子代码) + * @param {string} initLogGachaBaseType + * @param {number} page 页数 + * @param {string} endId 最后一个数据的 id + * @returns {Promise} 抽卡记录 + */ +export const getZZZGachaLogByAuthkey = async ( + authKey, + gachaType = '2001', + initLogGachaBaseType = '2', + page = 1, + endId = '0' +) => { + // 获取抽卡链接 + const link = await getZZZGachaLink( + authKey, + gachaType, + initLogGachaBaseType, + page, + endId + ); + // 发送请求 + const response = await request(link, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + retry: 3, + }); + // 获取数据 + const data = await response.json(); + if (!data || !data?.data) return null; + + return new ZZZGachaLogResp(data.data); +}; diff --git a/lib/gacha/tool.js b/lib/gacha/tool.js new file mode 100644 index 0000000..c306db7 --- /dev/null +++ b/lib/gacha/tool.js @@ -0,0 +1,19 @@ +/** + * 欧非分析 + * @param {number} ast + * @param {number[]} lst + */ +export const getLevelFromList = (ast, lst) => { + if (ast === 0) { + return 2; + } + + let level = 0; + for (let numIndex = 0; numIndex < lst.length; numIndex++) { + if (ast <= lst[numIndex]) { + level = 4 - numIndex; + break; + } + } + return level; +}; diff --git a/lib/mysapi.js b/lib/mysapi.js index e8489e2..932ccea 100644 --- a/lib/mysapi.js +++ b/lib/mysapi.js @@ -4,6 +4,7 @@ import crypto from 'crypto'; import ZZZApiTool from './mysapi/tool.js'; import { randomString } from '../utils/data.js'; import MysApi from '../../genshin/model/mys/mysApi.js'; +import { MysError } from './error.js'; // const DEVICE_ID = randomString(32).toUpperCase() // const DEVICE_NAME = randomString(_.random(1, 10)); @@ -18,6 +19,7 @@ export default class MysZZZApi extends MysApi { this.uid = uid; this.server = this.getServer(uid); this.apiTool = new ZZZApiTool(uid, this.server); + this.handler = option?.handler || {}; if (typeof this.cookie !== 'string' && this.cookie) { const ck = Object.values(this.cookie).find(item => { return item.ck && item.uid === uid; @@ -239,81 +241,32 @@ export default class MysZZZApi extends MysApi { * @param data 查询请求的数据 * @returns {Promise<*|boolean>} */ - async checkCode(e, res, type, data = {}) { - if (!res || !e) { - e.reply('米游社接口请求失败,暂时无法查询'); - return false; + async checkCode(res, type, data = {}) { + if (!res) { + throw new Error('米游社接口返回数据为空'); } - 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},米游社账号异常,暂时无法查询,请发送 %绑定设备帮助 查看如何绑定设备` + const code = String(res.retcode); + if (code === '1034' || code === '10035') { + // 如果有注册的mys.req.err,调用 + if (!!this?.handler && this?.handler?.has('mys.req.err')) { + logger.mark( + `[米游社绝区零查询失败][UID:${this.uid}][qq:${this.userId}] 遇到验证码,尝试调用 Handler mys.req.err` ); - 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; + res = + (await this.handler.call('mys.req.err', this.e, { + mysApi: this, + type, + res, + data, + mysInfo: this, + })) || res; } - 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}` - ); - throw new Error({ - type: 'mysApi', - uid: this.uid, - retcode: res.retcode, - result: res, - }); + if (code === '0') { + return res; } - return res; + throw new MysError(code, this.uid, res); } /** @@ -323,8 +276,8 @@ export default class MysZZZApi extends MysApi { * @param {{deviceFp?: string; query: Record; headers: object;}} data * @param {boolean} cached */ - async getFinalData(e, type, data = {}, cached = false) { - if (!data.headers) data.headers = {}; + async getFinalData(type, data = {}, cached = false) { + if (!data?.headers) data.headers = {}; if (data.deviceFp) { data.headers['x-rpc-device_fp'] = data.deviceFp; } @@ -364,7 +317,7 @@ export default class MysZZZApi extends MysApi { } } const result = await this.getData(type, data, cached); - const _data = await this.checkCode(e, result, type, {}); + const _data = await this.checkCode(result, type, {}); if (!_data || _data.retcode !== 0) return false; return _data.data; } diff --git a/lib/plugin.js b/lib/plugin.js index 1a48fb5..8d10061 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -75,7 +75,9 @@ export class ZZZPlugin extends plugin { } // 创建米游社 API 对象 - const api = new MysZZZApi(uid, ck); + const api = new MysZZZApi(uid, ck, { + handler: this.e?.runtime?.handler || {}, + }); const currentCK = Object.values(ck).find(item => { return item.ck && item.uid === uid; }); @@ -175,7 +177,11 @@ export class ZZZPlugin extends plugin { // 获取 米游社 API const { api, uid } = await this.getAPI(); // 获取用户信息 - let userData = await api.getFinalData(this.e, 'zzzUser'); + let userData = await api.getFinalData('zzzUser').catch(e => { + this.reply(e.message); + throw e; + }); + if (!userData) throw new Error('获取用户数据失败'); // 取第一个用户信息 userData =