mirror of
https://github.com/daydreamer-json/ak-endfield-api-archive.git
synced 2026-03-24 08:12:24 +01:00
feat: implement authentication test command and api utilities
This commit is contained in:
101
src/utils/api.ts
101
src/utils/api.ts
@@ -1,6 +1,6 @@
|
||||
import ky from 'ky';
|
||||
import semver from 'semver';
|
||||
import * as TypesApiAkEndfield from '../types/api/akEndfield/api.js';
|
||||
import * as TypesApiAkEndfield from '../types/api/akEndfield/Api.js';
|
||||
import appConfig from './config.js';
|
||||
|
||||
const defaultKySettings = {
|
||||
@@ -248,5 +248,104 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
accountService: {
|
||||
user: {
|
||||
auth: {
|
||||
v1: {
|
||||
tokenByEmailPassword: async (
|
||||
email: string,
|
||||
password: string,
|
||||
from: number = 0,
|
||||
): Promise<TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail> => {
|
||||
const rsp = await ky
|
||||
.post(
|
||||
`https://${appConfig.network.api.akEndfield.base.accountService}/user/auth/v1/token_by_email_password`,
|
||||
{ ...defaultKySettings, json: { email, from, password } },
|
||||
)
|
||||
.json();
|
||||
return rsp as TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail;
|
||||
},
|
||||
},
|
||||
},
|
||||
oauth2: {
|
||||
v2: {
|
||||
grant: async (
|
||||
appCode: string,
|
||||
token: string,
|
||||
type: number = 0,
|
||||
): Promise<TypesApiAkEndfield.AccSrvUserOAuth2V2Grant> => {
|
||||
const rsp = await ky
|
||||
.post(`https://${appConfig.network.api.akEndfield.base.accountService}/user/oauth2/v2/grant`, {
|
||||
...defaultKySettings,
|
||||
json: { appCode, token, type },
|
||||
})
|
||||
.json();
|
||||
return rsp as TypesApiAkEndfield.AccSrvUserOAuth2V2Grant;
|
||||
},
|
||||
},
|
||||
},
|
||||
info: {
|
||||
v1: {
|
||||
basic: async (appCode: string, token: string): Promise<TypesApiAkEndfield.AccSrvUserInfoV1Basic> => {
|
||||
const rsp = await ky
|
||||
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/basic`, {
|
||||
...defaultKySettings,
|
||||
searchParams: { appCode, token },
|
||||
})
|
||||
.json();
|
||||
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1Basic;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
u8: {
|
||||
user: {
|
||||
auth: {
|
||||
v2: {
|
||||
tokenByChToken: async (
|
||||
appCode: string,
|
||||
channelMasterId: number,
|
||||
channelToken: string,
|
||||
platform: number = 2,
|
||||
type: number = 0,
|
||||
): Promise<TypesApiAkEndfield.U8UserAuthV2ChToken> => {
|
||||
const rsp = await ky
|
||||
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/token_by_channel_token`, {
|
||||
...defaultKySettings,
|
||||
json: {
|
||||
appCode,
|
||||
channelMasterId,
|
||||
channelToken: JSON.stringify({
|
||||
code: channelToken,
|
||||
type: 1,
|
||||
isSuc: true,
|
||||
}),
|
||||
type,
|
||||
platform,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return rsp as TypesApiAkEndfield.U8UserAuthV2ChToken;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
game: {
|
||||
server: {
|
||||
v1: {
|
||||
serverList: async (token: string): Promise<TypesApiAkEndfield.U8GameServerV1ServerList> => {
|
||||
const rsp = await ky
|
||||
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/server/v1/server_list`, {
|
||||
...defaultKySettings,
|
||||
json: { token },
|
||||
})
|
||||
.json();
|
||||
return rsp as TypesApiAkEndfield.U8GameServerV1ServerList;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,8 +15,12 @@ type ConfigType = AllRequired<
|
||||
network: {
|
||||
api: {
|
||||
akEndfield: {
|
||||
appCode: { osWinRel: string };
|
||||
launcherAppCode: { osWinRel: string };
|
||||
appCode: {
|
||||
game: { osWinRel: string };
|
||||
launcher: { osWinRel: string };
|
||||
accountService: { osWinRel: string };
|
||||
u8: { osWinRel: string };
|
||||
};
|
||||
channel: { osWinRel: number };
|
||||
base: {
|
||||
accountService: string;
|
||||
@@ -55,8 +59,12 @@ const initialConfig: ConfigType = {
|
||||
network: {
|
||||
api: {
|
||||
akEndfield: {
|
||||
appCode: { osWinRel: 'YDUTE5gscDZ229CW' },
|
||||
launcherAppCode: { osWinRel: 'TiaytKBUIEdoEwRT' },
|
||||
appCode: {
|
||||
game: { osWinRel: 'YDUTE5gscDZ229CW' },
|
||||
launcher: { osWinRel: 'TiaytKBUIEdoEwRT' },
|
||||
accountService: { osWinRel: 'd9f6dbb6bbd6bb33' },
|
||||
u8: { osWinRel: '973bd727dd11cbb6ead8' },
|
||||
},
|
||||
channel: { osWinRel: 6 },
|
||||
base: {
|
||||
accountService: 'YXMuZ3J5cGhsaW5lLmNvbQ==',
|
||||
|
||||
148
src/utils/string.ts
Normal file
148
src/utils/string.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// === Generated by Qwen3-Max ===
|
||||
|
||||
/**
|
||||
* 元となるURLと相対URLから絶対URLを解決します。
|
||||
* ベースURLがファイルパスを含む場合でも、そのファイルのディレクトリを基準に解決します。
|
||||
*
|
||||
* @param baseUrl - 元となるURL(絶対URL)
|
||||
* @param relativeUrl - 相対URLまたは絶対URL
|
||||
* @returns 解決された絶対URL
|
||||
* @throws URLが無効な場合にエラーをスローします
|
||||
*/
|
||||
function resolveUrl(baseUrl: string, relativeUrl: string): string {
|
||||
try {
|
||||
// 相対URLがすでに絶対URLの場合、そのまま返す
|
||||
if (isAbsoluteUrl(relativeUrl)) {
|
||||
return new URL(relativeUrl).href;
|
||||
}
|
||||
|
||||
// baseUrlをURLとしてパース
|
||||
const base = new URL(baseUrl);
|
||||
|
||||
// ベースURLがファイルパス(末尾が/でない)の場合、
|
||||
// ファイル名を除去してディレクトリパスにする
|
||||
if (!base.pathname.endsWith('/')) {
|
||||
// 最後のスラッシュ以降を除去(ファイル名を削除)
|
||||
const lastSlashIndex = base.pathname.lastIndexOf('/');
|
||||
if (lastSlashIndex !== -1) {
|
||||
base.pathname = base.pathname.substring(0, lastSlashIndex + 1);
|
||||
} else {
|
||||
// パスにスラッシュが含まれていない稀なケース
|
||||
base.pathname = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 相対URLを解決
|
||||
const resolved = new URL(relativeUrl, base);
|
||||
|
||||
return resolved.href;
|
||||
} catch (error) {
|
||||
throw new Error(`URLの解決に失敗しました: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URLが絶対URLかどうかを判定します。
|
||||
*
|
||||
* @param url - 判定するURL文字列
|
||||
* @returns 絶対URLの場合はtrue、それ以外はfalse
|
||||
*/
|
||||
function isAbsoluteUrl(url: string): boolean {
|
||||
try {
|
||||
return Boolean(new URL(url).protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URLフルパスからファイル名を除いた部分を抽出します
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
function getBaseUrlWithoutLastSegment(url: string): string {
|
||||
const u = new URL(url);
|
||||
const dirPath = u.pathname.split('/').slice(0, -1).join('/');
|
||||
return `${u.origin}${dirPath}`;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
|
||||
function replaceMultiPatterns(replacements: [RegExp, string][], originalString: string): string {
|
||||
return replacements.reduce((currentString, [pattern, replacement]) => {
|
||||
return currentString.replace(pattern, replacement);
|
||||
}, originalString);
|
||||
}
|
||||
|
||||
function sanitizeFilename(filename: string) {
|
||||
const invalidCharsMap: Record<string, string> = {
|
||||
'\\': '\',
|
||||
'/': '/',
|
||||
':': ':',
|
||||
'*': '*',
|
||||
'?': '?',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'|': '|',
|
||||
'&': '&',
|
||||
};
|
||||
return Array.from(filename)
|
||||
.map((char) => invalidCharsMap[char] || char)
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字列が指定された正規表現パターンにマッチするかを判定する関数
|
||||
*
|
||||
* @param inputStr - チェック対象の文字列
|
||||
* @param regexInclude - マッチが必須の正規表現パターン(少なくとも1つマッチする必要あり)
|
||||
* @param regexExclude - マッチが禁止の正規表現パターン(1つもマッチしてはならない)
|
||||
* @returns
|
||||
* includeパターンが空の場合はexcludeにマッチしない限りtrue
|
||||
* includeパターンがある場合は、少なくとも1つのincludeにマッチし、
|
||||
* かつ全てのexcludeにマッチしない場合のみtrue
|
||||
*/
|
||||
function filterByRegex(
|
||||
inputStr: string,
|
||||
regexInclude: readonly (RegExp | string)[],
|
||||
regexExclude: readonly (RegExp | string)[],
|
||||
): boolean {
|
||||
// 文字列をRegExpに変換するヘルパー関数(グローバルフラグを無効化)
|
||||
const toRegExp = (pattern: RegExp | string): RegExp => {
|
||||
if (pattern instanceof RegExp) {
|
||||
// グローバルフラグ/ステッキーフラグを無効化(test()の状態問題を回避)
|
||||
const flags = pattern.flags.replace(/[gy]/g, '');
|
||||
return new RegExp(pattern.source, flags);
|
||||
}
|
||||
return new RegExp(pattern);
|
||||
};
|
||||
|
||||
// 正規表現を変換(include/exclude両方)
|
||||
const includePatterns = regexInclude.map(toRegExp);
|
||||
const excludePatterns = regexExclude.map(toRegExp);
|
||||
|
||||
// 1. excludeパターンに1つでもマッチしたら即座にfalse
|
||||
if (excludePatterns.some((pattern) => pattern.test(inputStr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. includeパターンが空の場合はtrue(excludeを通過した時点でOK)
|
||||
if (includePatterns.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. includeパターンに1つでもマッチすればtrue
|
||||
return includePatterns.some((pattern) => pattern.test(inputStr));
|
||||
}
|
||||
|
||||
// ==============================
|
||||
|
||||
export default {
|
||||
resolveUrl,
|
||||
isAbsoluteUrl,
|
||||
getBaseUrlWithoutLastSegment,
|
||||
replaceMultiPatterns,
|
||||
sanitizeFilename,
|
||||
filterByRegex,
|
||||
};
|
||||
46
src/utils/termPretty.ts
Normal file
46
src/utils/termPretty.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
const cliTableConfig = {
|
||||
rounded: {
|
||||
chars: {
|
||||
top: '─',
|
||||
'top-mid': '',
|
||||
'top-left': '╭',
|
||||
'top-right': '╮',
|
||||
bottom: '─',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '╰',
|
||||
'bottom-right': '╯',
|
||||
left: '│',
|
||||
'left-mid': '├',
|
||||
mid: '─',
|
||||
'mid-mid': '',
|
||||
right: '│',
|
||||
'right-mid': '┤',
|
||||
middle: '',
|
||||
},
|
||||
style: { 'padding-left': 1, 'padding-right': 1, head: [''], border: [''], compact: true },
|
||||
},
|
||||
noBorder: {
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: ' ',
|
||||
},
|
||||
style: { 'padding-left': 0, 'padding-right': 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
cliTableConfig,
|
||||
};
|
||||
Reference in New Issue
Block a user