mirror of
https://github.com/daydreamer-json/ak-endfield-api-archive.git
synced 2026-02-04 06:15:04 +01:00
Add auth test
This commit is contained in:
21
src/cmd.ts
21
src/cmd.ts
@@ -42,6 +42,27 @@ async function parseCommand() {
|
||||
},
|
||||
wrapHandler(cmds.test),
|
||||
)
|
||||
.command(
|
||||
['authTest [token] [email] [password]'],
|
||||
'Auth test command',
|
||||
(yargs) => {
|
||||
yargs
|
||||
.positional('token', {
|
||||
describe: 'Gryphline account service token',
|
||||
type: 'string',
|
||||
})
|
||||
.positional('email', {
|
||||
describe: 'Gryphline account email address',
|
||||
type: 'string',
|
||||
})
|
||||
.positional('password', {
|
||||
describe: 'Gryphline account password',
|
||||
type: 'string',
|
||||
})
|
||||
.options({});
|
||||
},
|
||||
wrapHandler(cmds.authTest),
|
||||
)
|
||||
.options({
|
||||
'log-level': {
|
||||
desc: 'Set log level (' + TypesLogLevels.LOG_LEVELS_NUM.join(', ') + ')',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import authTest from './cmds/authTest.js';
|
||||
import test from './cmds/test.js';
|
||||
|
||||
export default {
|
||||
authTest,
|
||||
test,
|
||||
};
|
||||
|
||||
163
src/cmds/authTest.ts
Normal file
163
src/cmds/authTest.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import chalk from 'chalk';
|
||||
import CliTable3 from 'cli-table3';
|
||||
import { HTTPError } from 'ky';
|
||||
import prompts from 'prompts';
|
||||
import apiUtils from '../utils/api.js';
|
||||
import argvUtils from '../utils/argv.js';
|
||||
import appConfig from '../utils/config.js';
|
||||
import exitUtils from '../utils/exit.js';
|
||||
import logger from '../utils/logger.js';
|
||||
import termPrettyUtils from '../utils/termPretty.js';
|
||||
|
||||
async function mainCmdHandler() {
|
||||
const cfg = appConfig.network.api.akEndfield;
|
||||
// const channelStr = String(cfg.channel.osWinRel);
|
||||
|
||||
let needRetrieveToken = false;
|
||||
let oauth2TokenPreRsp = null;
|
||||
if (!('token' in argvUtils.getArgv()) || !argvUtils.getArgv()['token']) {
|
||||
const tokenUserRsp: string = await (async () => {
|
||||
logger.warn('Gryphline account service token has not been specified. Requesting ...');
|
||||
const onCancelFn = () => {
|
||||
logger.error('Aborted');
|
||||
exitUtils.exit(1, null, false);
|
||||
};
|
||||
return (
|
||||
await prompts(
|
||||
{ name: 'value', type: 'password', message: 'Enter Gryphline account service token' },
|
||||
{ onCancel: onCancelFn },
|
||||
)
|
||||
).value;
|
||||
})();
|
||||
if (tokenUserRsp === '') {
|
||||
needRetrieveToken = true;
|
||||
} else {
|
||||
argvUtils.setArgv({ ...argvUtils.getArgv(), token: tokenUserRsp });
|
||||
}
|
||||
}
|
||||
if (needRetrieveToken === false) {
|
||||
try {
|
||||
oauth2TokenPreRsp = await apiUtils.apiAkEndfield.accountService.user.oauth2.v2.grant(
|
||||
cfg.appCode.accountService.osWinRel,
|
||||
argvUtils.getArgv()['token'],
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof HTTPError) {
|
||||
if ((await err.response.json()).status === 3) needRetrieveToken = true;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needRetrieveToken) {
|
||||
await (async () => {
|
||||
const onCancelFn = () => {
|
||||
logger.error('Aborted');
|
||||
exitUtils.exit(1, null, false);
|
||||
};
|
||||
if (!('email' in argvUtils.getArgv())) {
|
||||
logger.warn('Gryphline account email has not been specified. Requesting ...');
|
||||
const emailRsp: number = (
|
||||
await prompts(
|
||||
{
|
||||
...{ name: 'value', type: 'text', message: 'Enter Gryphline account email' },
|
||||
validate: (value) => (Boolean(value) ? true : 'Invalid value'),
|
||||
},
|
||||
{ onCancel: onCancelFn },
|
||||
)
|
||||
).value;
|
||||
argvUtils.setArgv({ ...argvUtils.getArgv(), email: emailRsp });
|
||||
}
|
||||
if (!('password' in argvUtils.getArgv())) {
|
||||
// logger.warn('Gryphline account password has not been specified. Requesting ...');
|
||||
const pwdRsp: number = (
|
||||
await prompts(
|
||||
{
|
||||
...{ name: 'value', type: 'password', message: 'Enter Gryphline account password' },
|
||||
validate: (value) => (Boolean(value) ? true : 'Invalid value'),
|
||||
},
|
||||
{ onCancel: onCancelFn },
|
||||
)
|
||||
).value;
|
||||
argvUtils.setArgv({ ...argvUtils.getArgv(), password: pwdRsp });
|
||||
}
|
||||
})();
|
||||
logger.info('Retrieving account service token ...');
|
||||
const accSrvTokenRsp = await apiUtils.apiAkEndfield.accountService.user.auth.v1.tokenByEmailPassword(
|
||||
argvUtils.getArgv()['email'],
|
||||
argvUtils.getArgv()['password'],
|
||||
);
|
||||
argvUtils.setArgv({ ...argvUtils.getArgv(), token: accSrvTokenRsp.data.token });
|
||||
}
|
||||
|
||||
logger.info('Retrieving account service OAuth 2.0 token ...');
|
||||
const oauth2TokenRsp = await apiUtils.apiAkEndfield.accountService.user.oauth2.v2.grant(
|
||||
cfg.appCode.accountService.osWinRel,
|
||||
argvUtils.getArgv()['token'],
|
||||
);
|
||||
logger.info('Retrieving u8 access token ...');
|
||||
const u8TokenRsp = await apiUtils.apiAkEndfield.u8.user.auth.v2.tokenByChToken(
|
||||
cfg.appCode.u8.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
oauth2TokenRsp.data.code,
|
||||
);
|
||||
logger.info('Authentication successful!');
|
||||
|
||||
logger.info('Retrieving user account data ...');
|
||||
const userAccData = await apiUtils.apiAkEndfield.accountService.user.info.v1.basic(
|
||||
cfg.appCode.accountService.osWinRel,
|
||||
argvUtils.getArgv()['token'],
|
||||
);
|
||||
logger.info('Retrieving user game server data ...');
|
||||
const userGameData = await apiUtils.apiAkEndfield.u8.game.server.v1.serverList(u8TokenRsp.data.token);
|
||||
|
||||
logger.info('Data retrieval completed!');
|
||||
|
||||
(() => {
|
||||
const table = new CliTable3(termPrettyUtils.cliTableConfig.rounded);
|
||||
table.push(
|
||||
...[
|
||||
['Account ID', userAccData.data.hgId],
|
||||
['Email', userAccData.data.realEmail],
|
||||
['Nickname', userAccData.data.nickName === '' ? chalk.dim('(none)') : userAccData.data.nickName],
|
||||
['Age Region', userAccData.data.ageGate.regionInfo['en-us']],
|
||||
].map((e) => [chalk.dim(e[0]), e[1]]),
|
||||
);
|
||||
console.log(table.toString());
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const table = new CliTable3(termPrettyUtils.cliTableConfig.rounded);
|
||||
table.push(
|
||||
...[
|
||||
['ID', 'Time', 'Name', 'Domain', 'Port'].map((e) => chalk.dim(e)),
|
||||
...userGameData.data.serverList.map((e) => [
|
||||
e.serverId,
|
||||
'UTC' + (JSON.parse(e.extension).offsetSeconds < 0 ? '' : '+') + JSON.parse(e.extension).offsetSeconds / 3600,
|
||||
e.serverName,
|
||||
JSON.parse(e.serverDomain)[0].host,
|
||||
JSON.parse(e.serverDomain)[0].port,
|
||||
]),
|
||||
],
|
||||
);
|
||||
console.log(table.toString());
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const table = new CliTable3(termPrettyUtils.cliTableConfig.rounded);
|
||||
table.push(
|
||||
...[
|
||||
['ID', 'UID', 'Level', 'Default'].map((e) => chalk.dim(e)),
|
||||
...userGameData.data.serverList.map((e) => [
|
||||
e.serverId,
|
||||
e.roleId,
|
||||
{ hAlign: 'right' as const, content: e.level },
|
||||
e.defaultChoose,
|
||||
]),
|
||||
],
|
||||
);
|
||||
console.log(table.toString());
|
||||
})();
|
||||
}
|
||||
|
||||
export default mainCmdHandler;
|
||||
@@ -81,8 +81,8 @@ async function mainCmdHandler() {
|
||||
await (async () => {
|
||||
logger.debug('Fetching latestGame ...');
|
||||
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
|
||||
cfg.appCode.osWinRel,
|
||||
cfg.launcherAppCode.osWinRel,
|
||||
cfg.appCode.game.osWinRel,
|
||||
cfg.appCode.launcher.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
@@ -103,8 +103,8 @@ async function mainCmdHandler() {
|
||||
);
|
||||
const prettyRsp = {
|
||||
req: {
|
||||
appCode: cfg.appCode.osWinRel,
|
||||
launcherAppCode: cfg.launcherAppCode.osWinRel,
|
||||
appCode: cfg.appCode.game.osWinRel,
|
||||
launcherAppCode: cfg.appCode.launcher.osWinRel,
|
||||
channel: cfg.channel.osWinRel,
|
||||
subChannel: cfg.channel.osWinRel,
|
||||
launcherSubChannel: cfg.channel.osWinRel,
|
||||
@@ -147,7 +147,7 @@ async function mainCmdHandler() {
|
||||
for (const versionInfoEntry of versionInfoList) {
|
||||
if (!versionInfoEntry.randStr) throw new Error('version rand_str not found');
|
||||
const rsp = await apiUtils.apiAkEndfield.launcher.latestGameResources(
|
||||
cfg.appCode.osWinRel,
|
||||
cfg.appCode.game.osWinRel,
|
||||
versionInfoEntry.versionMinor,
|
||||
versionInfoEntry.version,
|
||||
versionInfoEntry.randStr,
|
||||
@@ -155,7 +155,7 @@ async function mainCmdHandler() {
|
||||
logger.info(`Fetched latestGameRes: v${versionInfoEntry.version}, ${rsp.res_version}`);
|
||||
const prettyRsp = {
|
||||
req: {
|
||||
appCode: cfg.appCode.osWinRel,
|
||||
appCode: cfg.appCode.game.osWinRel,
|
||||
gameVersion: versionInfoEntry.versionMinor,
|
||||
version: versionInfoEntry.version,
|
||||
randStr: versionInfoEntry.randStr,
|
||||
@@ -178,7 +178,7 @@ async function mainCmdHandler() {
|
||||
const launcherTargetAppList = ['EndField', 'official'] as const;
|
||||
for (const launcherTargetAppEntry of launcherTargetAppList) {
|
||||
const rsp = await apiUtils.apiAkEndfield.launcher.latestLauncher(
|
||||
cfg.launcherAppCode.osWinRel,
|
||||
cfg.appCode.launcher.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
cfg.channel.osWinRel,
|
||||
null,
|
||||
@@ -187,7 +187,7 @@ async function mainCmdHandler() {
|
||||
logger.info(`Fetched latestLauncher: v${rsp.version}, ${launcherTargetAppEntry}`);
|
||||
const prettyRsp = {
|
||||
req: {
|
||||
appCode: cfg.launcherAppCode.osWinRel,
|
||||
appCode: cfg.appCode.launcher.osWinRel,
|
||||
channel: cfg.channel.osWinRel,
|
||||
subChannel: cfg.channel.osWinRel,
|
||||
targetApp: launcherTargetAppEntry === 'official' ? null : launcherTargetAppEntry,
|
||||
|
||||
@@ -106,6 +106,94 @@ type LauncherWebAnnouncement = {
|
||||
}[];
|
||||
};
|
||||
|
||||
type AccSrvUserAuthV1TokenByEmail = {
|
||||
data: {
|
||||
token: string;
|
||||
hgId: string; // hypergryph account id
|
||||
email: string; // obfuscated email
|
||||
isLatestUserAgreement: boolean;
|
||||
};
|
||||
msg: string;
|
||||
status: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type AccSrvUserInfoV1Basic = {
|
||||
data: {
|
||||
hgId: string; // hypergryph account id
|
||||
email: string; // obfuscated email
|
||||
realEmail: string; // un-obfuscated email
|
||||
isLatestUserAgreement: boolean;
|
||||
nickName: string;
|
||||
emailSubscription: boolean; // ???
|
||||
extension: { firebaseHashedInfo: string };
|
||||
ageGate: {
|
||||
ageAuthState: number;
|
||||
bindEmail: boolean;
|
||||
parentAuthState: number;
|
||||
regionInfo: Record<
|
||||
| 'de-de'
|
||||
| 'en-us' // Japan
|
||||
| 'es-mx'
|
||||
| 'fr-fr'
|
||||
| 'id-id'
|
||||
| 'it-it'
|
||||
| 'ja-jp' // 日本
|
||||
| 'ko-kr'
|
||||
| 'pt-br'
|
||||
| 'ru-ru'
|
||||
| 'th-th'
|
||||
| 'vi-vn'
|
||||
| 'zh-cn' // 日本
|
||||
| 'zh-tw',
|
||||
string
|
||||
>;
|
||||
regionCode: string; // JP
|
||||
};
|
||||
};
|
||||
msg: string;
|
||||
status: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type AccSrvUserOAuth2V2Grant = {
|
||||
data: {
|
||||
uid: string; // ???
|
||||
code: string; // this is channel token
|
||||
};
|
||||
msg: string; // OK, Login status expired.
|
||||
status: number; // 0=OK, 3=expired
|
||||
type: string;
|
||||
};
|
||||
|
||||
type U8UserAuthV2ChToken = {
|
||||
data: {
|
||||
token: string;
|
||||
isNew: boolean;
|
||||
uid: string; // number, game overall uid?
|
||||
};
|
||||
msg: string;
|
||||
status: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type U8GameServerV1ServerList = {
|
||||
data: {
|
||||
serverList: {
|
||||
serverId: string; // number
|
||||
serverName: string; // Asia
|
||||
serverDomain: string; // jsonStr [{"host": "beyond-asiapacific.gryphline.com", "port": 30000}]
|
||||
defaultChoose: boolean;
|
||||
roleId: string; // the so-called UID elsewhere
|
||||
level: number; // playerLv
|
||||
extension: string; // jsonStr {"offsetSeconds": -18000, "monthlyCardOffsetSecond": -18000}
|
||||
}[];
|
||||
};
|
||||
msg: string;
|
||||
status: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type {
|
||||
LauncherLatestGame,
|
||||
LauncherLatestGameResources,
|
||||
@@ -115,4 +203,9 @@ export type {
|
||||
LauncherWebMainBgImage,
|
||||
LauncherWebBanner,
|
||||
LauncherWebAnnouncement,
|
||||
AccSrvUserAuthV1TokenByEmail,
|
||||
AccSrvUserInfoV1Basic,
|
||||
AccSrvUserOAuth2V2Grant,
|
||||
U8UserAuthV2ChToken,
|
||||
U8GameServerV1ServerList,
|
||||
};
|
||||
|
||||
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