UNPKG

hashinami-cli

Version:
462 lines (440 loc) 18.2 kB
/** * @module MainConfig * @description Stores all basic configurations for the application. * * Available functions and objects: * - {@link MainConfig} - Stores general configuration values. * - {@link logging} - Manages application logging. * * @author hashinami46 */ import path from "path"; import boxen from "boxen"; import axios from "axios"; import crypto from "crypto"; import { homedir } from "os"; import i18next from "i18next"; import Table from "cli-table3"; import { Buffer } from "buffer"; import { fileURLToPath } from "url"; import decompress from "decompress"; import { spawn } from "child_process"; import stringWidth from "string-width"; import Backend from "i18next-fs-backend"; import DailyRotateFile from "winston-daily-rotate-file"; import { createLogger, format, /* transports */ } from "winston"; import { access, writeFile, readFile, mkdir, rm, readdir } from "fs/promises"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const { combine, timestamp, printf } = format; const packageJson = path.join(__dirname, "../package.json"); const { name, version, author, organization, repository } = JSON.parse(await readFile(packageJson, "utf8")); import { AppsLists } from "./RequestHandler/RequestEndpoint.js"; import { RequestAttribute } from "./RequestHandler/RequestAttribute.js"; /** * Object that contains basic informations and functions. * @namespace MainConfig * @property {string} appName - App name. * @property {string} appVersion - App version. * @property {string} appAuthor - The author of the app. * @property {string} appOrganization - App organization. * @property {string} appRepository - App repository. * @property {string} memberDataConfig - Member data location. * @property {string} credentialsConfig - Credentials location. * @property {string} langDir - Language asset directory. * @property {string} tempDir - Temporary asset directory. * @property {string} logsDir - Logs directory. * @property {string} rcdsDir - Asset record directory. * @property {string} ctlgDir - Catalog asset directory. * @property {string} saveDir - Asset download directory. * @property {string} backDir - Config backup directory. * @property {Function} createBox - Function to generate cli-box. * @property {Object} assetsRecord - Function to record downloaded assets. * @property {Function} nowTimestamp - Function to generate current timestamp in yyyy-mm-dd hh:MM:ss TZ format. * @property {Function} cheatSheet - Function to generate memberid. */ const MainConfig = { // About appName: name, appVersion: version, appAuthor: author, appOrganization: organization, appRepository: repository.url.replace("git+", ""), // Some important paths memberDataConfig: path.join(__dirname, "../.config/member.data.json"), credentialsConfig: path.join(__dirname, `../.config/secrets.credentials${process.env.HASHINAMI_APP_MODE === "dev" ? ".dev" : ""}.json`), langDir: path.join(__dirname, "../.config/locales"), tempDir: path.join(__dirname, "../.cache/temp"), //tempDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI"), ".cache/temp"), logsDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI"), ".cache/logs"), rcdsDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI"), ".cache/records"), ctlgDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI"), ".catalogs"), saveDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI")), backDir: path.join(process.env.HASHINAMI_LOCAL_DIR || (homedir() + "/HASHINAMI"), ".backups"), python_modules: path.join(__dirname, "../python_modules"), /** * Function to create and read file record. Useful if you don't wanna duplicate files to download. * @namespace assetsRecord * @memberOf MainConfig */ assetsRecord: { /** * Writes text record to the specified path. * @function write * @param {Object} options * @param {string} options.logpath - File records path. * @param {string} options.logstring - What string to record. * @return {Promise<void>} - This function doesn't return anything. * @memberOf MainConfig.assetsRecord */ write: async (options) => { await mkdir(path.dirname(options.logpath), { recursive: true }); if (await access(options.logpath).then(() => false).catch(() => true)) { await writeFile(options.logpath, "", "utf8"); }; const stringlist = (await readFile(options.logpath, "utf8")).split("\n").filter(stringlists => stringlists !== ""); stringlist.push(options.logstring); await writeFile( options.logpath, stringlist.join("\n"), "utf8" ); }, /** * Reads text record to the specified path. * @function read * @param {Object} options * @param {string} options.logpath - File records path. * @param {string} options.logstring - What string to record. * @return {Promise<boolean>} - This function return true or false. * @memberOf MainConfig.assetsRecord */ read: async (options) => { try { const stringlist = (await readFile(options.logpath, "utf8")).split("\n").filter(stringlists => stringlists !== ""); return stringlist.includes(options.logstring); } catch (err) { return false; }; } }, /** * Function to generate human readable timestamp. * @function nowTimestamp * @return {string} - Return current timestamp in yyyy-mm-dd hh:MM:ss TZ format. * @memberOf MainConfig */ nowTimestamp: () => { const now = new Date(); const datePart = now.toISOString().split("T")[0]; const timePart = now.toISOString().split("T")[1].split(".")[0]; const timeZone = now.toLocaleDateString(undefined, { day:"2-digit", timeZoneName: "long" }).substring(4).match(/\b(\w)/g).join(""); return `${datePart} ${timePart} ${timeZone}`; }, }; /** * Configures the application language settings using `i18n`. * * - Supports English (`en`), Indonesian (`id`), and Japanese (`ja`). * - Language files are stored in the directory specified by `MainConfig.langDir`. * - The default locale is determined by the `HASHINAMI_LANG` environment variable. * If the value is not in the supported locales, it defaults to English (`en`). * - Enables object notation for nested translations. * */ i18next .use(Backend) .init({ lng: ["en", "id", "ja"].includes(process.env.HASHINAMI_LANG) ? process.env.HASHINAMI_LANG : "en", fallbackLng: "en", preload: ["en", "id", "ja"], backend: { loadPath: path.join(MainConfig.langDir, "{{lng}}.json") }, initImmediate: false }); /** * Configure logging function. */ const logging = createLogger({ format: combine( timestamp(), printf(({ level, message, timestamp }) => { return `${timestamp} - ${MainConfig.appName} - ${level.toUpperCase()} - ${message}`; }) ), transports: [ new DailyRotateFile({ filename: path.join(MainConfig.logsDir, "logging-%DATE%.log"), datePattern: "YYYY-MM-DD", maxSize : "15m", maxFiles: "10d" }) ] }); /** * Function to create box in cli. * @param {Object} options - {logstring, logpath}. * @return {string} Generate box string. * @memberOf MainConfig */ MainConfig.createBox = (options) => { try { let text = ""; const [max_key, max_val] = Object.entries(options) .reduce(([maxKey, maxVal], [key, val]) => { return [ Math.max(maxKey, stringWidth(key)), Math.max(maxVal, stringWidth(val)) ]; }, [0, 0]); for (const [index, [key, value]] of Object.entries(options).entries()) { if (index === Object.entries(options).length - 1) { text += `${key.padEnd(max_key + 1 - stringWidth(key) + key.length)} : ${value.padEnd(max_val + 1 - stringWidth(value) + value.length)}`; } else { text += `${key.padEnd(max_key + 1 - stringWidth(key) + key.length)} : ${value.padEnd(max_val + 1 - stringWidth(value) + value.length)}\n`; }; }; logging.info(i18next.t("general.clibox.success")); console.info(boxen(text, { padding: 1, borderStyle: "round" })); } catch (err) { logging.error(err.name); logging.error(i18next.t("general.clibox.failed")); }; }; /** * Function to encrypt or decrypt data. * @namespace security * @memberOf MainConfig */ MainConfig.security = { key: crypto.createHash("sha512").update(process.env.HASHINAMI_KEY || "HASHINAMI").digest("hex").substring(0, 32), iv: crypto.createHash("sha512").update("b70673ab3cbf690159a1367ab11c20ec").digest("hex").substring(0, 16), encrypt: (data) => { try { const cipher = crypto.createCipheriv( "aes-256-cbc", MainConfig.security.key, MainConfig.security.iv ); logging.info(i18next.t("general.encrypt.success")); return Buffer.from( cipher.update(data, "utf8", "hex") + cipher.final("hex") ).toString("base64"); } catch (err) { logging.error(err.stack); logging.error(i18next.t("general.encrypt.failed")); console.error(i18next.t("general.encrypt.failed")); }; }, decrypt: (data) => { try { const decipher = crypto.createDecipheriv( "aes-256-cbc", MainConfig.security.key, MainConfig.security.iv ); logging.info(i18next.t("general.decrypt.success")); return ( decipher.update(Buffer.from(data, "base64").toString("utf8"), "hex", "utf8") + decipher.final("utf8") ); } catch (err) { logging.error(err.stack); logging.error(i18next.t("general.decrypt.success")); console.error(i18next.t("general.decrypt.success")); }; } }; /** * Function to asynchronously run spawn. * @namespace run * @memberOf MainConfig * @return {Promise<>} */ MainConfig.run = async (command, args) => { return new Promise((resolve, reject) => { const process = spawn(command, args, { stdio: "ignore" }); process.on("close", (code) => { if (code === 0) { resolve(); } else { logging.error(i18next.t("general.spawn.error", { command: command })); reject(i18next.t("general.spawn.error", { command: command })); }; }); }); }; /** * Function to generate available asset table. * @function cheatSheet * @return {Promise<void>} * @memberOf MainConfig */ MainConfig.cheatSheet = async ({ appname }) => { const memberdata = JSON.parse(await readFile(MainConfig.memberDataConfig, "utf8")); const membersdata = appname.includes("nogi") ? memberdata.nogizaka46 : appname.includes("saku") ? memberdata.sakurazaka46 : appname.includes("hina") ? memberdata.hinatazaka46 : appname === "unison" ? [...memberdata.sakurazaka46, ...memberdata.hinatazaka46] : appname === "asukatalk" ? memberdata.saitoasuka : appname === "maiyantalk" ? memberdata.shiraishimai : appname === "mizukitalk" ? memberdata.yamashitamizuki : appname === "centforcetalk" ? memberdata.centforce : memberdata.yodel const cheatsheet = new Table({ 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": "|" }, head: appname.includes("talk") && Object.keys(membersdata[0]).includes(appname.replace("talk", "blog")) ? ["talk", "blog", "漢字"] : appname.includes("talk") ? ["talk", "漢字"] : [appname, "漢字"], colAligns: appname.includes("talk") && Object.keys(membersdata[0]).includes(appname.replace("talk", "blog")) ? ["right", "right"] : ["right", "left"] }); membersdata.forEach((item, index) => { const apptype = appname.includes("talk") ? item[appname.replace("talk", "msg")] : item[appname] const appblog = appname.includes("talk") && item[appname.replace("talk", "blog")] ? item[appname.replace("talk", "blog")] : undefined const checkblog = appname.includes("talk") && Object.keys(membersdata[index]).includes(appname.replace("talk", "blog")); if (checkblog && apptype && appblog) { cheatsheet.push([apptype, appblog, item.name]) return }; if (checkblog && apptype && !appblog) { cheatsheet.push([apptype, "", item.name]) return }; if (!checkblog && apptype) { cheatsheet.push([apptype, item.name]) return }; }); const appMemberId = `${i18next.t("parameters.info.title_memberid")}\n${cheatsheet.toString()}\n`; const appTitle = AppsLists.messagesList.filter(app => ["nogitalk", "sakutalk", "hinatalk"].includes(app)).includes(appname) ? `${i18next.t("parameters.info.title_apptitle")}\n ${i18next.t("parameters.appname.mobameblog.title")}\n` : AppsLists.messagesList.filter(app => !["nogitalk", "sakutalk", "hinatalk"].includes(app)).includes(appname) ? `${i18next.t("parameters.info.title_apptitle")}\n ${i18next.t("parameters.appname.mobame.title")}\n` : `${i18next.t("parameters.info.title_apptitle")}\n ${i18next.t(`parameters.appname.${appname}.title`)}\n` const appName = `${i18next.t("parameters.info.title_appname")}\n ${appname}\n` const commonAssetList = ["Image", "Video", "Unity", "Usm", "Cpk", "Config", "Catalog"] .flatMap(asset => AppsLists[`common${asset}List`] ) .filter(asset => asset.includes(appname)) .map(asset => asset.replace(`${appname}_`, " - ")); const appAsset = AppsLists.messagesList.filter(app => ["nogitalk", "sakutalk", "hinatalk"].includes(app)).includes(appname) ? `${i18next.t("parameters.info.title_availableasset")}\n - timeline\n - past_messages\n - blogs\n` : AppsLists.messagesList.filter(app => !["nogitalk", "sakutalk", "hinatalk"].includes(app)).includes(appname) ? `${i18next.t("parameters.info.title_availableasset")}\n - timeline\n - past_messages\n` : `${i18next.t("parameters.info.title_availableasset")}\n${commonAssetList.join("\n")}\n` const sampleMember = appname === "nogitalk" ? "柴田柚菜" : appname === "sakutalk" ? "小島凪紗" : appname === "hinatalk" ? "小西夏菜実" : appname === "yodeltalk" ? "樋口日奈" : appname === "centforcetalk" ? "新内眞衣" : appname === "sakukoi" ? "中嶋優月" : appname === "hinakoi" ? "森本茉莉" : appname === "unison" ? "中嶋優月" : undefined const sampleUsageList = ["nogitalk", "sakutalk", "hinatalk"].includes(appname) ? [ ` hashinami-cli -A ${appname} -T timeline --parallel`, ` hashinami-cli -A ${appname} -T timeline -f 2025-03-01 -t 2025-04-01 -M ${sampleMember} --parallel`, ` hashinami-cli -A ${appname} -T past_messages -M ${sampleMember} --parallel`, ` hashinami-cli -A ${appname} -T blogs --parallel` ] : ["yodeltalk", "centforcetalk"].includes(appname) ? [ ` hashinami-cli -A ${appname} -T timeline --parallel`, ` hashinami-cli -A ${appname} -T timeline -f 2025-03-01 -t 2025-04-01 -M ${sampleMember} --parallel`, ` hashinami-cli -A ${appname} -T past_messages -M ${sampleMember} --parallel`, ] : ["asukatalk", "maiyantalk", "mizukitalk"].includes(appname) ? [ ` hashinami-cli -A ${appname} -T timeline --parallel`, ` hashinami-cli -A ${appname} -T timeline -f 2025-03-01 -t 2025-04-01 --parallel`, ` hashinami-cli -A ${appname} -T past_messages --parallel`, ] : appname === "nogikoi" ? [ ` hashinami-cli -A ${appname} -T card_png -st 8 -c pink -f 12000 -t 12100 --parallel`, ` hashinami-cli -A ${appname} -T sprites -M 柴田柚菜 -sr 4 -c -f 120 -t 130 --parallel` ] : appname === "nogifes" ? [ ` hashinami-cli -A ${appname} -T photo_common -f 12812 -t 12850 --parallel`, ` hashinami-cli -A ${appname} -T movie_card -f 700 -t 720 --parallel`, ` hashinami-cli -A ${appname} -T reward_movie -f 2890 -t 2900 --parallel`, ] : ["sakukoi", "hinakoi"].includes(appname) ? [ ` hashinami-cli -A ${appname} -T movie -f 3000 -t 3299 --disablefilter --parallel`, ` hashinami-cli -A ${appname} -T card -f 4000 -t 4299 --parallel`, ` hashinami-cli -A ${appname} -T card -M ${sampleMember} -f 4000 -t 4299 --parallel`, ] : appname === "unison" ? [ ` hashinami-cli -A ${appname} -T chara_movie -f 1 -t 200 --parallel`, ` hashinami-cli -A ${appname} -T card_movie -M ${sampleMember} -f 1 -t 200 --parallel`, ] : [] const appSampleUsage = `${i18next.t("parameters.info.title_sampleusage")}\n${sampleUsageList.map((item, index) => index === sampleUsageList.length - 1 ? item : item + "\n" ).join("")}` const catalogId = appname === "unison" ? "20250228120634" : "225022801" const appAdvancedUsage = AppsLists.messagesList.includes(appname) ? `\n${i18next.t("parameters.info.title_advancedusage")}\n hashinami-cli -A ${appname} --refresh-token <your-refresh-token>` : ["unison", "sakukoi", "hinakoi"].includes(appname) ? `\n${i18next.t("parameters.info.title_advancedusage")}\n hashinami-cli -A ${appname} -T catalog -C ${catalogId}` : "" let refreshToken = ""; if (appname.includes("talk")) { const { refresh_token } = await new RequestAttribute(appname).authToken({ mode: "read" }); if (refresh_token) { refreshToken = `\n${i18next.t("parameters.info.title_refreshtoken")}\n ${refresh_token.replace(/-(?!.*-).*/, (match) => match[0] + "*".repeat(match.length - 1))}` }; }; console.log([appTitle, appName, appAsset, appMemberId, appSampleUsage, appAdvancedUsage, refreshToken].join("")); }; /** * Function to generate available asset table. * @function listFileRecursive * @return {Promise<void>} * @memberOf MainConfig */ MainConfig.listFileRecursive = async (dirpath) => { let listFile = []; const entries = await readdir(dirpath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirpath, entry.name); if (entry.isDirectory()) { const nestedFiles = await MainConfig.listFileRecursive(fullPath); listFile = listFile.concat(nestedFiles); } else if (entry.isFile()) { listFile.push(fullPath); }; }; return listFile }; export { MainConfig, logging, i18next };