기존
This commit is contained in:
56
src/utils/Config.ts
Normal file
56
src/utils/Config.ts
Normal 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
112
src/utils/Database.ts
Normal 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
38
src/utils/Logger.ts
Normal 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")
|
||||
}
|
||||
16
src/utils/Prod-commands.ts
Normal file
16
src/utils/Prod-commands.ts
Normal 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
35
src/utils/Transcode.ts
Normal 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
51
src/utils/tts/Chzzk.ts
Normal 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
21
src/utils/tts/Google.ts
Normal 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
24
src/utils/tts/Utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const def_replaceObj: Record<string, string> = {
|
||||
"\\?": "물음표",
|
||||
"\\!": "느낌표",
|
||||
"\\~": "물결",
|
||||
"\\+": "더하기",
|
||||
"\\-": "빼기",
|
||||
"\\(": "여는소괄호",
|
||||
"\\)": "닫는소괄호",
|
||||
"\\{": "여는중괄호",
|
||||
"\\}": "닫는중괄호",
|
||||
"\\[": "여는대괄호",
|
||||
"\\]": "닫는대괄호",
|
||||
"ㄹㅇ": "리얼",
|
||||
"ㄲㅂ": "까비",
|
||||
"ㅎㅇ": "하이",
|
||||
"ㅇㅋ": "오키",
|
||||
"ㄴㅇㅅ": "나이스",
|
||||
"ㅇㅈ": "인정",
|
||||
"ㅅㄱ": "수고",
|
||||
"ㅇㅎ": "아하",
|
||||
"ㄱㄱ": "기역기역",
|
||||
"ㅃㄹ": "빨리",
|
||||
"ㄱㅊ": "괜찮",
|
||||
};
|
||||
Reference in New Issue
Block a user