koishi-plugin-enka-fork
Version:
Enka.Network for koishi
398 lines (397 loc) • 18.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Config = exports.inject = exports.name = void 0;
exports.apply = apply;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const koishi_1 = require("koishi");
const puppeteer_page_proxy_1 = __importDefault(require("puppeteer-page-proxy"));
const localeMap_json_1 = __importDefault(require("./locales/localeMap.json"));
const zh_json_1 = __importDefault(require("./locales/zh.json"));
const types_1 = require("./types");
const UUIDRegExp = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
exports.name = 'enka';
exports.inject = ['puppeteer', 'database'];
exports.Config = koishi_1.Schema.object({
agent: koishi_1.Schema.union([
koishi_1.Schema.const(types_1.EnkaAgent.ENKA).description('默认(Enka)'),
koishi_1.Schema.const(types_1.EnkaAgent.ENKA).description('默认(Enka)'),
]).default(types_1.EnkaAgent.ENKA).description('请求地址'),
data: koishi_1.Schema.union([
koishi_1.Schema.const(types_1.EnkaDataAgent.NYAN).description('NyanZone'),
koishi_1.Schema.const(types_1.EnkaDataAgent.GITHUB).description('GitHub'),
koishi_1.Schema.const(types_1.EnkaDataAgent.GHPROXY).description('Proxy(GH)'),
]).default(types_1.EnkaDataAgent.NYAN).description('数据地址'),
proxy: koishi_1.Schema.union([
koishi_1.Schema.const(false).description('禁止'),
koishi_1.Schema.const(true).description('全局设置'),
koishi_1.Schema.string().role('link').description('自定义'),
]).description('Puppetter 代理设置,仅用于 Puppeteer, 不会影响其他请求(⚠实验性)'),
});
function mapIndexSearch(index, search) {
for (const key in index) {
if (key.includes(search))
return index[key];
}
return false;
}
function apply(ctx, config) {
ctx.i18n.define('zh', zh_json_1.default);
ctx.model.extend('user', {
genshin_uid: 'string(20)',
enka_data: 'json',
});
ctx.model.extend('enka_alias', {
cid: 'string(20)',
alias: 'list',
}, {
primary: ['cid'],
unique: ['cid'],
});
const logger = ctx.logger('enka');
let page;
let lock;
const mapIndex = {};
let map = {};
let characterInfo = {};
async function initlization(forcibly = false) {
logger.debug('checking characters data...');
const dataPath = path_1.default.join(ctx.root.baseDir, 'data/enka/idMap.json');
const characterPath = path_1.default.join(ctx.root.baseDir, 'data/enka/characters.json');
const aliasData = await ctx.database.get('enka_alias', {});
const update = async () => {
if (forcibly)
logger.debug('forcibly update characters data...');
// download UIGF characters data
logger.debug('downloading UIGF ID map...');
const data = await ctx.http.get('https://api.uigf.org/dict/genshin/all.json');
for (let locale in data) {
locale = locale.toLowerCase();
const characters = data[locale];
if (locale === 'chs')
locale = 'zh';
if (locale === 'cht')
locale = 'zh-tw';
for (const characterName in characters) {
const id = characters[characterName];
if (id < 10000000)
continue;
if (!map[id])
map[id] = { names: { [locale]: characterName } };
else
map[id].names[locale] = characterName;
}
}
// download enka characters data
logger.debug('downloading characters data...');
const enka = await ctx.http.get(`${config.data}/characters.json`);
await promises_1.default.mkdir('data/enka', { recursive: true });
await promises_1.default.writeFile(characterPath, JSON.stringify(enka));
await promises_1.default.writeFile(dataPath, JSON.stringify(map));
};
if (forcibly)
await update();
else {
try {
await promises_1.default.access(dataPath);
await promises_1.default.access(characterPath);
// load UIGF ID and Characters data
logger.debug('characters data exists, loading...');
const mapBuf = await promises_1.default.readFile(dataPath);
const dataBuf = await promises_1.default.readFile(characterPath);
map = JSON.parse(mapBuf.toString());
characterInfo = JSON.parse(dataBuf.toString());
}
catch (error) {
logger.debug('characters data not exists, mapping...');
await update();
}
}
for (const id in map) {
const alias = aliasData.find((i) => i.cid === id)?.alias || [];
const characterNames = Object.values(map[id].names).flatMap((nameList) => (Array.isArray(nameList) ? nameList : [nameList]));
mapIndex[[...characterNames, ...alias].join(',')] = id;
}
logger.info('characters data loaded.');
}
ctx.on('ready', async () => {
page || (page = await ctx.puppeteer.page());
if (config.proxy)
await (0, puppeteer_page_proxy_1.default)(page, config.proxy === true ? ctx.root.config.request.proxyAgent : config.proxy);
logger.info('initlizing puppeteer.');
await initlization();
logger.debug('all initlized.');
});
ctx.command('enka [search:string]')
.userFields(['genshin_uid', 'locales', 'enka_data'])
.option('update', '-u')
.action(async ({ session, options }, search) => {
const locale = (session.user.locale || session.user.locales[0]) ?? 'zh';
const userLang = localeMap_json_1.default[locale] || ['简体中文', '自定义文本'];
logger.debug('search:', search);
if (!session.user.genshin_uid)
return session.text('.bind');
if (search && !mapIndexSearch(mapIndex, search))
return session.text('.non-existent');
search = mapIndexSearch(mapIndex, search);
logger.debug('search to id:', search || 'null');
logger.debug('userLang:', userLang);
if (Object.keys(session.user.enka_data).length === 0 || options.update) {
try {
// 添加延迟防止频繁请求
await new Promise((resolve) => {
setTimeout(resolve, 1500);
});
const response = await ctx.http.get(`${config.agent}/api/uid/${session.user.genshin_uid}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; KoishiBot/1.0; +https://github.com/koishijs/koishi-plugin-enka)',
'Accept': 'application/json',
},
});
logger.debug('Enka API response:', JSON.stringify(response, null, 2));
if (!response) {
logger.warn('Empty response from Enka API');
return session.text('.error');
}
const info = response.playerInfo;
logger.debug('Player info:', info ? JSON.stringify(info, null, 2) : 'null');
if (!info) {
logger.warn('No player info found in response');
return session.text('.no-data');
}
if (!info.showAvatarInfoList) {
logger.warn('No showAvatarInfoList found in player info');
return session.text('.no-characters');
}
if (info.showAvatarInfoList.length === 0) {
logger.warn('showAvatarInfoList is empty');
return session.text('.no-characters');
}
session.user.enka_data = {
characterList: info.showAvatarInfoList.map((i) => i.avatarId),
characterLevels: info.showAvatarInfoList,
...(0, koishi_1.pick)(info, ['nickname', 'level', 'signature', 'worldLevel']),
};
}
catch (error) {
logger.error(`Enka API error: ${error.message}`);
logger.error('Error details:', error);
if (error.response?.status === 403) {
return session.text('.forbidden');
}
if (error.response?.status === 404) {
return session.text('.no-data');
}
if (error.response?.status === 429) {
return session.text('.rate-limited');
}
if (error.response?.status >= 500) {
return session.text('.server-error');
}
return session.text('.error');
}
}
// ... 后续代码不变 ...
// now, the 'search' is a character id
logger.debug('Sending relax message...');
await session.send(session.text('.relax'));
logger.debug('Checking lock...');
if (lock) {
logger.debug('Waiting for lock...');
await lock;
}
logger.debug('Creating new lock...');
let resolve;
lock = new Promise((r) => {
resolve = r;
});
logger.debug('Processing search:', search);
if (search) {
logger.debug('Search mode - character:', characterInfo[search]);
if (!characterInfo[search]) {
logger.debug('Character not found in characterInfo');
return session.text('.non-existent');
}
// check this search in user's character list
logger.debug('Checking character in user list:', Number(search));
logger.debug('User character list:', session.user.enka_data.characterList);
if (!session.user.enka_data.characterList.includes(Number(search))) {
logger.debug('Character not in user list');
return session.text('.non-existent');
}
// get character image
logger.debug('Starting character image generation...');
try {
logger.debug('Navigating to Enka page...');
await page.goto(`${config.agent}/u/${session.user.genshin_uid}/`, {
waitUntil: 'networkidle0',
timeout: 60000,
});
logger.debug('Page loaded successfully');
const { left, top } = await page.evaluate(async (searchParam, userLangParam) => {
Array.from((document.querySelectorAll('.UI.SelectorElement')))
.find((i) => i.innerHTML.trim() === userLangParam)?.click();
for (const i of Array.from((document.querySelectorAll('.Dropdown-list')))) {
i.style.display = 'none';
}
const tabs = Array.from(document.getElementsByTagName('figure'));
const _characters = [];
for (const ele of tabs) {
if (ele.style.backgroundImage?.includes('AvatarIcon')) {
_characters.push(ele.style.backgroundImage?.replace('url("/ui/', '').replace('.png")', ''));
}
}
if (searchParam) {
const select = tabs.find((i) => i.style.backgroundImage?.toLowerCase().includes(searchParam.SideIconName.toLowerCase()));
const rect = select?.parentElement?.getBoundingClientRect();
for (const i of Array.from((document.querySelectorAll('.Checkbox.Control.sm:not(.checked)')))) {
i.click();
}
if (!select)
return { left: 0, top: 0 };
return { left: rect?.left || 0, top: rect?.top || 0 };
}
return { left: 0, top: 0 };
}, characterInfo[search], userLang[0]);
logger.debug('Page evaluation result:', { left, top });
if (!left) {
logger.debug('No character found on page');
return session.text('.not-found');
}
logger.debug('Clicking character position...');
await page.mouse.click(left + 1, top + 1);
await Promise.all([
page.waitForNetworkIdle({ idleTime: 100 }),
page.evaluate((inText) => {
const input = document.querySelector(`[placeholder="${inText || 'Custom text'}"]`);
if (input) {
input.value = 'Koishi & Enka Network';
input.dispatchEvent(new Event('input'));
}
return null;
}, userLang[1]),
]);
logger.debug('Clicking image button...');
await page.click('button[data-icon="image"]');
logger.debug('Waiting for image response...');
const buf = await new Promise((resolvePromise) => {
const cb = async (ev) => {
logger.debug('Response URL:', ev.request().url());
if (!UUIDRegExp.test(ev.request().url().trim()))
return;
logger.debug('Found image response, resolving...');
page.off('response', cb);
resolvePromise(await ev.buffer());
};
page.on('response', cb);
});
logger.debug('Image buffer received, size:', buf.length);
return koishi_1.h.image(buf, 'image/png');
}
catch (error) {
logger.error('Character image generation error:', error);
return session.text('.error');
}
finally {
logger.debug('Resolving lock...');
resolve();
}
}
else {
logger.debug('List mode - showing character list');
const { nickname, level, signature, worldLevel, characterLevels, } = session.user.enka_data;
logger.debug('user_data:', session.user.enka_data);
let title = `<p>${session.text('.list', [nickname, level, signature, worldLevel])}</p>`;
const content = [];
let tLength = 1;
logger.debug('characterLevels:', characterLevels);
if (characterLevels.length > 0) {
for (const character of characterLevels) {
const namer = map[character.avatarId];
if (character) {
const n = namer.names[locale || 'zh'];
if (n.length > tLength)
tLength = n.length;
content.push({
namer: n, level: character.level,
});
}
}
}
else {
title = session.text('.non-list');
}
logger.debug('Resolving lock for list mode...');
resolve();
logger.debug('Returning character list');
return title + content.map((i) => `<p>(${i.level.toString().padStart(2, '0')}) ${i.namer}</p>`).join('');
}
});
// 先注册子命令,避免与主命令冲突
ctx.command('enka.uid.show')
.alias('enka.uid.info')
.userFields(['genshin_uid'])
.action(async ({ session }) => {
if (!session.user.genshin_uid) {
return session.text('.not-bound');
}
return session.text('.uid', [session.user.genshin_uid]);
});
ctx.command('enka.uid.rm')
.alias('enka.uid.remove')
.userFields(['genshin_uid', 'enka_data'])
.action(async ({ session }) => {
if (!session.user.genshin_uid) {
return session.text('.not-bound');
}
const removedUid = session.user.genshin_uid;
session.user.genshin_uid = '';
session.user.enka_data = {
nickname: '',
level: 0,
signature: '',
worldLevel: 0,
characterList: [],
characterLevels: [],
};
return session.text('.removed', [removedUid]);
});
ctx.command('enka.uid.add <uid:string>')
.alias('enka.uid.set')
.userFields(['genshin_uid'])
.action(async ({ session }, uid) => {
if (!/^[1256789][0-9]{3,9}$/gm.test(uid))
return session.text('.fail');
if (uid === session.user.genshin_uid)
return session.text('.same');
session.user.genshin_uid = uid;
session.send(session.text('.saved', [uid]));
});
ctx.command('enka.upgrade')
.alias('.up')
.action(async ({ session }) => {
session.send(session.text('.upgrading'));
await initlization(true);
return session.text('.upgraded');
});
ctx.command('enka.alias <name:string> <alias:string>')
.action(async ({ session }, characterName, alias) => {
const cid = mapIndexSearch(mapIndex, characterName);
if (!cid)
return session.text('.non-existent');
const aliasTable = await ctx.database.get('enka_alias', { cid });
if (aliasTable.length === 0)
aliasTable.push({ cid, alias: [alias] });
else {
if (aliasTable[0].alias.includes(alias))
return session.text('.exist');
aliasTable[0].alias.push(alias);
}
await ctx.database.upsert('enka_alias', aliasTable);
initlization();
return session.text('.saved', [alias, characterName]);
});
}