This commit is contained in:
2026-05-26 14:15:09 +09:00
parent 55d402f606
commit d6b36c43c2
33 changed files with 1496 additions and 0 deletions

56
src/utils/Config.ts Normal file
View File

@@ -0,0 +1,56 @@
import "dotenv/config";
import { join } from "node:path";
import { def_replaceObj } from "./tts/Utils";
export const Config = {
_appId: process.env.APPID?.trim(),
_token: process.env.TOKEN?.trim(),
_prefix: process.env.PREFIX?.trim(),
_dbPath: process.env.DBPATH?.trim(),
_guildId: process.env.GUILDID?.trim(),
_chzzk: {
nid_aut: process.env.CHZZK_NID_AUT?.trim(),
nid_ses: process.env.CHZZK_NID_SES?.trim(),
},
_ttsPath: process.env.TTSPATH?.trim(),
debug: process.env.DEBUG?.trim()?.toLocaleLowerCase() === "true",
dev: process.env.DEV?.trim()?.toLocaleLowerCase() === "true",
replaceObj: { ...def_replaceObj, ...JSON.parse(process.env.REPLACETEXT?.trim() || "[{}]")[0] },
get appId() {
if (!this._appId) throw new ReferenceError("APPID is missing");
return this._appId;
},
get token() {
if (!this._token) throw new ReferenceError("TOKEN is missing");
return this._token;
},
get prefix() {
if (!this._prefix) throw new ReferenceError("PREFIX is missing");
return this._prefix;
},
get dbPath() {
if (!this._dbPath) throw new ReferenceError("DBPATH is missing");
return this._dbPath;
},
get guildId() {
if (!this._guildId) throw new ReferenceError("GUILDID is missing");
return this._guildId;
},
get chzzk() {
if (!this._chzzk.nid_aut) throw new ReferenceError("CHZZK_NID_AUT is missing");
if (!this._chzzk.nid_ses) throw new ReferenceError("CHZZK_NID_SES is missing");
return {
nid_aut: this._chzzk.nid_aut,
nid_ses: this._chzzk.nid_ses,
};
},
get ttsPath() {
if (!this._ttsPath) throw new ReferenceError("TTSPATH is missing");
return this._ttsPath;
}
};
// 명령어 폴더
export const COMMANDS_PATH = join(__dirname, "..", "commands");
export const COMMAND_PATH = (commandFile: string) => join(COMMANDS_PATH, commandFile);

112
src/utils/Database.ts Normal file
View File

@@ -0,0 +1,112 @@
import Database from "better-sqlite3";
import { Config } from "./Config";
import { dirname, join } from "node:path";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { GuildType, UserType } from "../../db/db";
import { Logger } from "./Logger";
const dbDir = dirname(Config.dbPath); // './db' 추출
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
const database = new Database(Config.dbPath);
const schemaPath = join(process.cwd(), "db/schema.sql");
const schema = readFileSync(schemaPath, "utf-8");
database.exec(schema);
Logger.ready("DB 활성화!");
const stmt = {
guild: {
// 가져오기
get: database.prepare("SELECT * FROM guilds WHERE ID = ?"),
// 추가
insert: (data: GuildType) => {
const keys = Object.keys(data);
if (keys.length === 0) throw new Error("insert: 키1개는 있어야함");
return database.prepare(`INSERT INTO guilds (${
keys.map(k => `"${k}"`).join(", ")
}) VALUES (${
keys.map(k => `@${k}`).join(", ")
})`).run(data);
},
// 수정
update: (data: GuildType) => {
const keys = Object.keys(data).filter(k => k !== "id");
if (keys.length === 0) throw new Error("update: 키1개는 있어야함");
return database.prepare(`UPDATE guilds SET ${
keys.map(k => `${k} = @${k}`).join(", ")
} WHERE id = @id`).run(data);
},
},
user: {
// 가져오기
get: database.prepare("SELECT * FROM users WHERE guild_id = ? AND id = ?"),
// 추가
insert: (data: UserType) => {
const keys = Object.keys(data);
if (keys.length === 0) throw new Error("insert: 키1개는 있어야함");
return database.prepare(`INSERT INTO users (${
keys.map(k => `"${k}"`).join(", ")
}) VALUES (${
keys.map(k => `@${k}`).join(", ")
})`).run(data);
},
// 수정
update: (data: UserType) => {
const keys = Object.keys(data).filter(k => k !== "guild_id" && k !== "id");
if (keys.length === 0) throw new Error("update: 키1개는 있어야함");
return database.prepare(`UPDATE guilds SET ${
keys.map(k => `${k} = @${k}`).join(", ")
} WHERE guild_id = @guild_id AND id = @id`).run(data);
},
},
};
export const DB = {
guild: {
get(id: string) {
return stmt.guild.get.get(id) as GuildType | undefined;
},
set(data: GuildType) {
try {
stmt.guild.insert(data);
return true;
} catch (err) {
Logger.error(String(err));
return false;
}
},
update(data: GuildType) {
try {
stmt.guild.update(data);
return true;
} catch (err) {
Logger.error(String(err));
return false;
}
},
},
user: {
get(guildId: string, id: string) {
return stmt.user.get.get(guildId, id) as UserType | undefined;
},
set(data: UserType) {
try {
stmt.user.insert(data);
return true;
} catch (err) {
Logger.error(String(err));
return false;
}
},
update(data: UserType) {
try {
stmt.user.update(data);
return true;
} catch (err) {
Logger.error(String(err));
return false;
}
},
},
};

38
src/utils/Logger.ts Normal file
View File

@@ -0,0 +1,38 @@
import colors from "colors/safe";
export const Timestamp = () => {
const Now = new Date();
Now.setHours(Now.getHours() + 9);
return Now.toISOString().replace('T', ' ').substring(0, 19).slice(2);
}
type logType = "log" | "info" | "warn" | "error" | "debug" | "ready" | "slash";
const log = (content: string, type: logType) => {
const timestamp = colors.white(`[${Timestamp()}]`);
switch (type) {
case "log":
return console.log(`${colors.gray("[LOG]")} ${timestamp} ${content}`);
case "info":
return console.log(`${colors.cyan("[INFO]")} ${timestamp} ${content}`);
case "warn":
return console.log(`${colors.yellow("[WARN]")} ${timestamp} ${content}`);
case "error":
return console.log(`${colors.red("[ERROR]")} ${timestamp} ${content}`);
case "debug":
return console.log(`${colors.magenta("[DEBUG]")} ${timestamp} ${content}`);
case "ready":
return console.log(`${colors.green("[READY]")} ${timestamp} ${content}`);
default:
throw new TypeError("Logger 타입이 올바르지 않습니다.");
}
};
export const Logger = {
log: (content: string) => log(content, "log"),
warn: (content: string) => log(content, "warn"),
error: (content: string) => log(content, "error"),
debug: (content: string) => log(content, "debug"),
info: (content: string) => log(content, "info"),
ready: (content: string) => log(content, "ready")
}

View File

@@ -0,0 +1,16 @@
import { REST, Routes } from "discord.js";
import { Config } from "./Config";
import { handler } from "../index";
import { Logger } from "./Logger";
async function main() {
const rest = new REST({ version: "10" }).setToken(Config.token);
const body = Array.from(handler.commands.values().filter(cmd => cmd.visible).map(cmd => cmd.metaData));
// 전역 등록 (권장: 배포 파이프라인에서만 실행)
await rest.put(Routes.applicationCommands(Config.appId), { body });
Logger.ready(`전역 슬래시 등록 요청 완료: ${body.length}`);
}
main().catch((err) => {
throw err instanceof Error ? err : new Error(String(err));
});

35
src/utils/Transcode.ts Normal file
View File

@@ -0,0 +1,35 @@
import ffmpegPath from "ffmpeg-static";
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { Readable } from "node:stream";
import { AudioResource, createAudioResource, StreamType } from "@discordjs/voice";
/** MP3 Buffer -> PCM(s16le 48k 2ch) Readable stream */
function mp3BufferToPcmStream(mp3Buf: Buffer): Readable {
if (!ffmpegPath) throw new Error("ffmpeg-static 경로 확인 실패");
const ff: ChildProcessWithoutNullStreams = spawn(ffmpegPath, [
"-loglevel","quiet","-hide_banner",
"-i","pipe:0", // stdin으로 mp3
"-f","s16le", // raw PCM
"-ar","48000", // 48k
"-ac","2", // stereo
"pipe:1" // stdout으로 PCM
], { stdio: ["pipe","pipe","pipe"] });
// 입력 밀어넣고 닫기
// ff.stdin.write(mp3Buf);
// ff.stdin.end();
Readable.from(mp3Buf).pipe(ff.stdin);
// ffmpeg stdout(PCM)을 그대로 리턴
// (에러 로그가 필요하면 ff.stderr 'data' 핸들링 추가)
return ff.stdout;
}
export function createResourceFromMp3Buffer(mp3Buf: Buffer): AudioResource {
const pcmStream = mp3BufferToPcmStream(mp3Buf);
return createAudioResource(pcmStream, {
inputType: StreamType.Raw,
inlineVolume: false, // 필요하면 true로 하고 volume 조절
});
}

51
src/utils/tts/Chzzk.ts Normal file
View File

@@ -0,0 +1,51 @@
import axios from "axios";
import { Config } from "../Config";
export enum VoiceType {
// 현재(25.08.27)
= "yuna",
= "garam",
= "dain",
= "meow",
= "kyungtae",
= "mammon",
= "seungpyo",
= "woosik",
= "sangdo",
// 이전
= "hajun",
};
const defHeaders = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Cookie": Object.entries(Config.chzzk).map(([key, value]) => key+'='+value).join(';'),
};
const getToken = () => new Promise<string>((res, rej) => {
axios.get("https://api.chzzk.naver.com/service/v1/alerts/token", {
headers: defHeaders,
responseType: "json"
}).then((val) => {
if (!val.data?.content?.token) return rej(val.data?.message || "오류");
return res(val.data.content.token);
}).catch((err) => {
return rej(err.response?.data?.message || "오류");
});
});
export const textToSpeech = (text: string, voice: VoiceType) => new Promise<Buffer>(async (res, rej) => {
axios.post("https://api.chzzk.naver.com/service/v1/alerts/tts", {
message: text,
token: await getToken().catch(() => "undefined"),
type: "n"+voice
}, {
headers: defHeaders,
responseType: "arraybuffer"
}).then((val) => {
if (val.status !== 200) return rej(val.data?.message || "오류");
return res(val.data);
}).catch((err) => {
return rej(err.response?.data?.message || "오류");
});
});

21
src/utils/tts/Google.ts Normal file
View File

@@ -0,0 +1,21 @@
import axios from "axios";
const SPEED = 0.5;
export const textToSpeech = (text: string) => new Promise<Buffer>(async (res, rej) => {
axios.get(`https://www.google.com/speech-api/v1/synthesize?${new URLSearchParams({
"text": text,
"lang": "ko-kr",
"speed": SPEED.toString()
}).toString()}`, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
},
responseType: "arraybuffer"
}).then((val) => {
if (val.status !== 200) return rej("오류");
return res(val.data);
}).catch(() => {
return rej("오류");
});
});

24
src/utils/tts/Utils.ts Normal file
View File

@@ -0,0 +1,24 @@
export const def_replaceObj: Record<string, string> = {
"\\?": "물음표",
"\\!": "느낌표",
"\\~": "물결",
"\\+": "더하기",
"\\-": "빼기",
"\\(": "여는소괄호",
"\\)": "닫는소괄호",
"\\{": "여는중괄호",
"\\}": "닫는중괄호",
"\\[": "여는대괄호",
"\\]": "닫는대괄호",
"ㄹㅇ": "리얼",
"ㄲㅂ": "까비",
"ㅎㅇ": "하이",
"ㅇㅋ": "오키",
"ㄴㅇㅅ": "나이스",
"ㅇㅈ": "인정",
"ㅅㄱ": "수고",
"ㅇㅎ": "아하",
"ㄱㄱ": "기역기역",
"ㅃㄹ": "빨리",
"ㄱㅊ": "괜찮",
};