diff --git a/bot/db/db.d.ts b/bot/db/db.d.ts index 386c079..0be043a 100644 --- a/bot/db/db.d.ts +++ b/bot/db/db.d.ts @@ -7,10 +7,4 @@ export interface GuildType { recommend: boolean; }; } -export type GuildRow = Omit & { options: string }; - -// export interface UserType { -// guild_id: string; -// id: string; -// name: string; -// } \ No newline at end of file +export type GuildRow = Omit & { options: string }; \ No newline at end of file diff --git a/bot/package.json b/bot/package.json index 7bce364..39df2b1 100644 --- a/bot/package.json +++ b/bot/package.json @@ -20,7 +20,6 @@ "typescript": "^6.0.2" }, "dependencies": { - "axios": "^1.14.0", "better-sqlite3": "^12.8.0", "colors": "^1.4.0", "discord.js": "^14.25.1", diff --git a/bot/src/classes/BotClient.ts b/bot/src/classes/BotClient.ts index 6fedfdf..0575888 100644 --- a/bot/src/classes/BotClient.ts +++ b/bot/src/classes/BotClient.ts @@ -1,5 +1,6 @@ import { Client, ClientEvents, ColorResolvable, EmbedBuilder, EmbedField, GatewayIntentBits, Message } from "discord.js"; import { Config } from "../utils/Config"; +import { Logger } from "../utils/Logger"; export class BotClient extends Client { public prefix = Config.prefix; @@ -69,8 +70,10 @@ export class BotClient extends Client { setTimeout(async () => { try { const msg = await message.fetch(true).catch(() => undefined); - if (msg?.deletable) msg.delete().catch(() => {}); - } catch {}; + if (msg?.deletable) msg.delete().catch((err) => { + Logger.warn(`[BotClient] 메세지 삭제 실패: ${String(err)}`); + }); + } catch {} }, Math.max(100, time * (customTime ? 1 : 6000))); } } \ No newline at end of file diff --git a/bot/src/classes/GuildPlayer.ts b/bot/src/classes/GuildPlayer.ts index 1bd7067..9948ec5 100644 --- a/bot/src/classes/GuildPlayer.ts +++ b/bot/src/classes/GuildPlayer.ts @@ -5,7 +5,7 @@ import { timeFormat } from "../utils/music/Utils"; import { default_content, default_embed, default_image, getButtons } from "../utils/music/Config"; import { GuildType } from "../../db/db"; import { DB } from "../utils/Database"; -import { shuffle } from "../utils/Shuffle"; +import { shuffle } from "../utils/shuffle"; import { checkTextChannelAndMsg } from "../utils/music/Channel"; import { Logger } from "../utils/Logger"; @@ -22,6 +22,7 @@ export class GuildPlayer { public queue: QueueTrack[] = []; private errorTimer: NodeJS.Timeout | undefined; private endTimer: NodeJS.Timeout | undefined; + private closedTimer: NodeJS.Timeout | undefined; constructor( public guild: Guild, @@ -32,45 +33,51 @@ export class GuildPlayer { ) { this.player.setGlobalVolume(50); this.player.on("start", (_data: TrackStartEvent) => { + // endTimer가 남아있으면 제거 (새 곡 재생 시작) + if (this.endTimer !== undefined) { + clearTimeout(this.endTimer); + this.endTimer = undefined; + } Redis?.publishState("player_update", { guildId: this.guild.id, }); }); this.player.on("end", async (data: TrackEndEvent) => { - if (this.isDead) return; - if (data.reason === "replaced") return; - // 방금 끝난 곡을 대기열에서 지우면서 lastPlayedTrack에 저장 - this.lastPlayedTrack = this.queue.shift(); - if (this.queue.length > 0) { - await this.playNext(); - } else if (this.isRecommend && this.canRecommend) { - await this.autoPlay(); - } else { - this.end(); + try { + if (this.isDead) return; + if (data.reason === "replaced") return; + // 방금 끝난 곡을 대기열에서 지우면서 lastPlayedTrack에 저장 + this.lastPlayedTrack = this.queue.shift(); + if (this.queue.length > 0) { + await this.playNext(); + } else if (this.isRecommend && this.canRecommend) { + await this.autoPlay(); + } else { + this.end(); + } + } catch (err) { + Logger.error(`[GuildPlayer] end 이벤트 ��리 중 에러: ${String(err)}`); } }); this.player.on("closed", () => { if (this.isDead) return; Logger.info(`[GuildPlayer] 음성 연결이 끊어졌습니다. 재접속을 대기합니다...`); - setTimeout(() => { + // 이전 closed 타이머가 있으면 제거 + if (this.closedTimer !== undefined) { + clearTimeout(this.closedTimer); + } + this.closedTimer = setTimeout(() => { + this.closedTimer = undefined; if (this.isDead) return; // 5초가 지났는데도 연결이 복구되지 않았을 때만 방을 나갑니다. - + // 디스코드 방에 내 봇(me)이 없으면 봇을 삭제(delete)한다! if (!this.guild.members.me?.voice?.channelId) { Logger.warn(`[GuildPlayer] 음성채널에 봇이 없습니다. player를 초기화합니다.`); return this.delete(); } - /** - * declare enum State { - * CONNECTING = 0, - * CONNECTED = 1, - * DISCONNECTING = 2, - * DISCONNECTED = 3 - * } - */ - // (1 = CONNECTED, Shoukaku 버전에 따라 연결 상태 체크가 다를 수 있으니 안전하게 확인) + // (1 = CONNECTED, Shoukaku 버전에 따라 연결 상태 체크가 다를 수 있으�� 안전하게 확인) if (this.player && this.player.node.state !== 1) { Logger.warn(`[GuildPlayer] 연결 복구 실패. 봇을 퇴장시킵니다.`); return this.delete(); @@ -79,12 +86,20 @@ export class GuildPlayer { }); this.player.on("exception", async (data) => { - Logger.error(`[Lavalink] 재생 중 에러 발생: ${data.exception?.message}`); - await this.errMsg("유튜브 차단 또는 재생 오류로 인해 이 곡을 건너뜁니다."); + try { + Logger.error(`[Lavalink] 재생 중 에러 발생: ${data.exception?.message}`); + await this.errMsg("유튜브 차단 또는 재생 오류로 인해 이 곡을 건너뜁니다."); + } catch (err) { + Logger.error(`[GuildPlayer] exception 이벤트 처리 중 에러: ${String(err)}`); + } }); this.player.on("stuck", async (data) => { - Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과`); - await this.errMsg("음원 로딩이 멈췄습니다. 다음 곡으로 넘어갑니다."); + try { + Logger.error(`[Lavalink] 곡 로딩 멈춤(Stuck) 발생: ${data.thresholdMs}ms 초과`); + await this.errMsg("음원 로딩이 멈췄습니다. 다음 곡으로 넘어갑니다."); + } catch (err) { + Logger.error(`[GuildPlayer] stuck 이벤트 처리 중 에러: ${String(err)}`); + } }); } @@ -214,16 +229,31 @@ export class GuildPlayer { this.end(); return; } - if (tracks[0].info.identifier === trackId) tracks = tracks.slice(1); + if (tracks.length > 0 && tracks[0].info.identifier === trackId) tracks = tracks.slice(1); + if (tracks.length === 0) { + this.end(); + return; + } this.addTracks(tracks, "자동재생"); } - public end() { + private clearAllTimers() { if (this.errorTimer !== undefined) { clearTimeout(this.errorTimer); this.errorTimer = undefined; } - if (this.endTimer !== undefined) clearTimeout(this.endTimer); + if (this.endTimer !== undefined) { + clearTimeout(this.endTimer); + this.endTimer = undefined; + } + if (this.closedTimer !== undefined) { + clearTimeout(this.closedTimer); + this.closedTimer = undefined; + } + } + + public end() { + this.clearAllTimers(); this.endTimer = setTimeout(() => { this.endTimer = undefined; this.delete(true); @@ -238,7 +268,10 @@ export class GuildPlayer { if (this.isDead) return; if (!afterEnd) this.end(); this.isDead = true; - this.player.destroy().catch(() => {}); + this.clearAllTimers(); + this.player.destroy().catch((err) => { + Logger.error(`[GuildPlayer] player.destroy 에러: ${String(err)}`); + }); lavalinkManager.delPlayer(this.guild.id); lavalinkManager.shoukaku.leaveVoiceChannel(this.guild.id); } diff --git a/bot/src/classes/Handler.ts b/bot/src/classes/Handler.ts index d0d0451..54506ff 100644 --- a/bot/src/classes/Handler.ts +++ b/bot/src/classes/Handler.ts @@ -2,6 +2,7 @@ import { ChatInputCommandInteraction, Collection } from "discord.js"; import { readdirSync } from "node:fs"; import { Command } from "../types/Command"; import { COMMAND_PATH, COMMANDS_PATH } from "../utils/Config"; +import { Logger } from "../utils/Logger"; export class Handler { public commands: Collection = new Collection(); @@ -15,11 +16,16 @@ export class Handler { } } - public runCommand(interaction: ChatInputCommandInteraction) { + public async runCommand(interaction: ChatInputCommandInteraction) { const commandName = interaction.commandName; const command = this.commands.get(commandName); if (!command) return; - if (command.slashRun) command.slashRun(interaction); + try { + if (command.slashRun) await command.slashRun(interaction); + } catch (err) { + Logger.error(`[Handler] 명령어 '${commandName}' 실행 중 에러: ${String(err)}`); + await interaction.editReply({ content: "명령어 실행 중 오류가 발생했습니다." }).catch(() => {}); + } } } \ No newline at end of file diff --git a/bot/src/classes/LavalinkManager.ts b/bot/src/classes/LavalinkManager.ts index 7f51c73..fc6cbae 100644 --- a/bot/src/classes/LavalinkManager.ts +++ b/bot/src/classes/LavalinkManager.ts @@ -4,7 +4,7 @@ import { GuildPlayer } from "./GuildPlayer"; import { Config } from "../utils/Config"; import { Logger } from "../utils/Logger"; import { parseLink } from "../utils/music/Url"; -import { shuffle } from "../utils/Shuffle"; +import { shuffle } from "../utils/shuffle"; import { Spotify } from "../utils/api/Spotify"; import { YoutubeMusic } from "../utils/api/YoutubeMusic"; diff --git a/bot/src/classes/RedisClient.ts b/bot/src/classes/RedisClient.ts index b290a61..aeec4b9 100644 --- a/bot/src/classes/RedisClient.ts +++ b/bot/src/classes/RedisClient.ts @@ -148,8 +148,10 @@ class RedisClientClass { const numIndex = Number(data.index); if (isNaN(numIndex)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index 타입이 올바르지 않습니다." })); if (numIndex < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index는 0보다 크거나 같아야합니다." })); - if (numIndex >= context.player.queue.length-1) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index가 queue.length보다 클수 없습니다." })); - const [removedTrack] = context.player.queue.splice(numIndex+1, 1); + // queue[0]은 현재 재생중인 곡이므로 실제 대기열은 queue[1]부터 시작 + // numIndex는 대기열(queue[1]~) 기준이므로 실제 splice 위치�� numIndex+1 + if (numIndex >= context.player.queue.length - 1) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index가 대기열 범위를 초과합니다." })); + const [removedTrack] = context.player.queue.splice(numIndex + 1, 1); await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, removedTrack })); context.player.setMsg(); } @@ -179,8 +181,8 @@ class RedisClientClass { if (!data.seek) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek를 찾을수 없습니다." })); const context = await this.getContext(data.serverId, resultKey, data.userId); if (!context.ok) return; - if (!context.player.isPlaying || !context.player.nowTrack) await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." })); - const duration = context.player.nowTrack?.info.length || 0; + if (!context.player.isPlaying || !context.player.nowTrack) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." })); + const duration = context.player.nowTrack.info.length || 0; const numSeek = Number(data.seek); if (isNaN(numSeek)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek 타입이 올바르지 않습니다." })); if (numSeek < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek는 0보다 크거나 같아야합니다." })); @@ -195,7 +197,7 @@ class RedisClientClass { if (!data.volume) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume을 찾을수 없습니다." })); const context = await this.getContext(data.serverId, resultKey, data.userId); if (!context.ok) return; - if (!context.player.isPlaying || !context.player.nowTrack) await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." })); + if (!context.player.isPlaying || !context.player.nowTrack) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." })); const numVolume = Number(data.volume); if (isNaN(numVolume)) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume 타입이 올바르지 않습니다." })); if (numVolume < 0) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume은 0보다 크거나 같아야합니다." })); diff --git a/bot/src/commands/channel.ts b/bot/src/commands/channel.ts index d2951ca..eed961c 100644 --- a/bot/src/commands/channel.ts +++ b/bot/src/commands/channel.ts @@ -4,6 +4,7 @@ import { Command } from "../types/Command"; import { clearAllMsg } from "../utils/music/Utils"; import { default_content, default_embed, default_image, getButtons } from "../utils/music/Config"; import { DB } from "../utils/Database"; +import { Logger } from "../utils/Logger"; /** channel 명령어 */ export default class implements Command { @@ -100,7 +101,7 @@ export async function channelRegister(guild: Guild | null, channelId: string | n components: [ getButtons() ], files: [ default_image ], }).catch((err) => { - console.error(err); + Logger.error(`[Channel] 메세지 생성 실패: ${String(err)}`); return null; }); if (!msg) return client.mkembed({ diff --git a/bot/src/commands/join.ts b/bot/src/commands/join.ts index 8f1f722..5434e00 100644 --- a/bot/src/commands/join.ts +++ b/bot/src/commands/join.ts @@ -72,7 +72,7 @@ export async function channelJoin(guild: Guild | null, voiceChannelId: string | }) }; let player = lavalinkManager.getPlayer(guild.id); - if (player) return { embed: client.mkembed({ title: `이미 <#${player.voiceChannelId} 참가중입니다.` }), player }; + if (player) return { embed: client.mkembed({ title: `이미 <#${player.voiceChannelId}> 참가중입니다.` }), player }; player = new GuildPlayer( guild, await lavalinkManager.shoukaku.joinVoiceChannel({ diff --git a/bot/src/events/interactionCreate.ts b/bot/src/events/interactionCreate.ts index 23c6e17..714c137 100644 --- a/bot/src/events/interactionCreate.ts +++ b/bot/src/events/interactionCreate.ts @@ -1,36 +1,47 @@ import { Interaction, MessageFlags } from "discord.js"; import { handler } from "../index"; import { buttonInteraction } from "../utils/music/Button"; +import { Logger } from "../utils/Logger"; export const interactionCreate = async (interaction: Interaction) => { - if (interaction.isStringSelectMenu()) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - const commandName = interaction.customId; - const args = interaction.values; - const command = handler.commands.get(commandName); - if (command?.menuRun) return await command.menuRun(interaction, args); + try { + if (interaction.isStringSelectMenu()) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch((err) => { + Logger.warn(`[Interaction] SelectMenu deferReply 실패: ${String(err)}`); + }); + const commandName = interaction.customId; + const args = interaction.values; + const command = handler.commands.get(commandName); + if (command?.menuRun) return await command.menuRun(interaction, args); + } + + if (interaction.isButton()) { + const args = interaction.customId.split("-"); + if (!args || args.length === 0) return; + + if (args[0] === "music") return buttonInteraction(interaction, args.slice(1)); + + await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch((err) => { + Logger.warn(`[Interaction] Button deferReply 실패: ${String(err)}`); + }); + + const key = args.shift(); + if (!key) return; + const command = handler.commands.get(key); + if (command?.buttonRun) return command.buttonRun(interaction, args); + } + + if (!interaction.isChatInputCommand()) return; + + /** + * 명령어 친사람만 보이게 설정 + * flags: MessageFlags.Ephemeral + */ + await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch((err) => { + Logger.warn(`[Interaction] Command deferReply 실패: ${String(err)}`); + }); + await handler.runCommand(interaction); + } catch (err) { + Logger.error(`[Interaction] 처리 중 에러: ${String(err)}`); } - - if (interaction.isButton()) { - const args = interaction.customId.split("-"); - if (!args || args.length === 0) return; - - if (args[0] === "music") return buttonInteraction(interaction, args.slice(1)); - - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - - const key = args.shift(); - if (!key) return; - const command = handler.commands.get(key); - if (command?.buttonRun) return command.buttonRun(interaction, args); - } - - if (!interaction.isChatInputCommand()) return; - - /** - * 명령어 친사람만 보이게 설정 - * flags: MessageFlags.Ephemeral - */ - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - handler.runCommand(interaction); } \ No newline at end of file diff --git a/bot/src/events/voiceStateUpdate.ts b/bot/src/events/voiceStateUpdate.ts index 33d1b01..ed7a93c 100644 --- a/bot/src/events/voiceStateUpdate.ts +++ b/bot/src/events/voiceStateUpdate.ts @@ -1,5 +1,3 @@ +// TODO: 음성 상태 변경 이벤트 핸들러 (추후 구현) // import { VoiceState } from "discord.js"; -// import { client } from "../index"; - -// export const voiceStateUpdate = async (oldState: VoiceState, newState: VoiceState): Promise => { -// } \ No newline at end of file +// export const voiceStateUpdate = async (oldState: VoiceState, newState: VoiceState): Promise => {} diff --git a/bot/src/types/Command.d.ts b/bot/src/types/Command.d.ts index 1614605..c029781 100644 --- a/bot/src/types/Command.d.ts +++ b/bot/src/types/Command.d.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, ChatInputApplicationCommandData, ChatInputChatInputCommandInteraction, Message, StringSelectMenuInteraction } from "discord.js"; +import { ButtonInteraction, ChatInputApplicationCommandData, ChatInputCommandInteraction, Message, StringSelectMenuInteraction } from "discord.js"; export interface Command { /** 메세지 이름 */ @@ -13,9 +13,9 @@ export interface Command { * 등록 메타: JSON 변환된 바디 * (빌드 시 toJSON()해서 REST 등록에 사용) */ - metaData: RESTPostAPIChatInputApplicationCommandsJSONBody; + metaData: ChatInputApplicationCommandData; - slashRun?: (args: ChatInputChatInputCommandInteraction) => Promise; + slashRun?: (args: ChatInputCommandInteraction) => Promise; messageRun?: (message: Message, args: string[]) => Promise; menuRun?: (interaction: StringSelectMenuInteraction, args: string[]) => Promise; buttonRun?: (interaction: ButtonInteraction, args: string[]) => Promise; diff --git a/bot/src/utils/Config.ts b/bot/src/utils/Config.ts index c098904..91a3187 100644 --- a/bot/src/utils/Config.ts +++ b/bot/src/utils/Config.ts @@ -69,6 +69,8 @@ export const Config = { return this._youtube_cookie; }, + proxyUrl: process.env.PROXY_URL?.trim() || "", + _redis: { state: process.env.REDIS?.trim()?.toLocaleLowerCase() === "true", host: process.env.REDIS_HOST?.trim(), diff --git a/bot/src/utils/Database.ts b/bot/src/utils/Database.ts index b985391..92339d4 100644 --- a/bot/src/utils/Database.ts +++ b/bot/src/utils/Database.ts @@ -7,12 +7,18 @@ import { Logger } from "./Logger"; const database = new Database(Config.dbPath); -const schemaPath = join(process.cwd(), "db/schema.sql"); +const schemaPath = join(__dirname, "../../db/schema.sql"); const schema = readFileSync(schemaPath, "utf-8"); database.exec(schema); Logger.ready("DB 활성화!"); +// 허용되는 guild 테이블 컬럼 화이트리스��� +const GUILD_COLUMNS = new Set(["id", "name", "channel_id", "msg_id", "options"]); + +const filterKeys = (keys: string[], whitelist: Set) => + keys.filter(k => whitelist.has(k)); + const stmt = { guild: { // 전체 @@ -21,7 +27,7 @@ const stmt = { get: database.prepare("SELECT * FROM guilds WHERE ID = ?"), // 추가 insert: (data: GuildRow) => { - const keys = Object.keys(data); + const keys = filterKeys(Object.keys(data), GUILD_COLUMNS); if (keys.length === 0) throw new Error("insert: 키1개는 있어야함"); return database.prepare(`INSERT INTO guilds (${ keys.map(k => `"${k}"`).join(", ") @@ -31,35 +37,13 @@ const stmt = { }, // 수정 update: (data: GuildRow) => { - const keys = Object.keys(data).filter(k => k !== "id"); + const keys = filterKeys(Object.keys(data), GUILD_COLUMNS).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 users SET ${ - // keys.map(k => `${k} = @${k}`).join(", ") - // } WHERE guild_id = @guild_id AND id = @id`).run(data); - // }, - // }, }; export const DB = { @@ -91,27 +75,4 @@ export const DB = { } }, }, - // 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; - // } - // }, - // }, }; \ No newline at end of file diff --git a/bot/src/utils/api/Spotify.ts b/bot/src/utils/api/Spotify.ts index fbd1099..ceca9c6 100644 --- a/bot/src/utils/api/Spotify.ts +++ b/bot/src/utils/api/Spotify.ts @@ -8,7 +8,7 @@ const SPOTIFY_SECRET = process.env.SPOTIFY_SECRET?.trim() ?? ""; const SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"; const SPOTIFY_API_URL = "https://api.spotify.com/v1"; -const TOKENR_URL = "http://192.168.10.5:8075/api/token"; +const TOKENR_URL = process.env.SPOTIFY_TOKENER_URL?.trim() || "http://192.168.10.5:8075/api/token"; const searchCache = new Map(); diff --git a/bot/src/utils/api/YoutubeMusic.ts b/bot/src/utils/api/YoutubeMusic.ts index 470b1b9..f21cd7d 100644 --- a/bot/src/utils/api/YoutubeMusic.ts +++ b/bot/src/utils/api/YoutubeMusic.ts @@ -3,10 +3,11 @@ import crypto from "node:crypto"; import { Cookies } from "../../types/Youtube_Cookie"; import { Config } from "../Config"; import { SongItem } from "../../types/Track"; +import { Logger } from "../Logger"; const customPREF = "tz=Asia.Seoul&hl=ko&gl=KR&last_quality=1080"; export const ORIGIN = "https://music.youtube.com"; -const proxy = new ProxyAgent('http://192.168.10.4:3128'); +const proxy = Config.proxyUrl ? new ProxyAgent(Config.proxyUrl) : undefined; const searchCache = new Map(); // 🌟 클래스 외부에 둘 상수 및 유틸리티 함수들 (내부에서만 사용됨) @@ -56,7 +57,7 @@ export const YoutubeMusic = { const missing = keys.filter((k) => !(k in cookies) && !(blocks ?? []).includes(k)); if (missing.length > 0) { - console.log("현재 입력된 쿠키 키 목록:", Object.keys(cookies)); + Logger.warn(`현재 입력된 쿠키 키 목록: ${Object.keys(cookies).join(", ")}`); throw new Error(`❌ 필수 인증 쿠키가 누락되었습니다: ${missing.join(", ")}`); } @@ -82,7 +83,7 @@ export const YoutubeMusic = { * 완벽한 쿠키 인증과 서명(SAPISIDHASH)을 사용하여 유튜브 뮤직 검색을 수행합니다. */ async getSearchFull(query: string): Promise { - console.log(`🔍 [Auth-Cookie Engine] "${query}" 데이터 추출 중 (썸네일, 재생시간 포함)...`); + Logger.log(`🔍 [Auth-Cookie Engine] "${query}" 데이터 추출 중 (썸네일, 재생시간 포함)...`); const url = "https://music.youtube.com/youtubei/v1/search?prettyPrint=false"; @@ -109,7 +110,7 @@ export const YoutubeMusic = { query: query, params: "EgWKAQIIAWoOEAMQBBAQEAkQFRAKEBE=" }), - dispatcher: proxy + ...(proxy ? { dispatcher: proxy } : {}) }); const data: any = await response.json(); @@ -200,9 +201,9 @@ export const YoutubeMusic = { } } - return results || []; // 배열이 비어있을 경우 안전하게 null 반환 + return results; } catch (error) { - console.error("❌ getSearchFull 실행 중 에러:", error); + Logger.error(`❌ getSearchFull 실행 중 에러: ${String(error)}`); return []; } }, diff --git a/bot/src/utils/music/Button.ts b/bot/src/utils/music/Button.ts index f879fc6..5db3d05 100644 --- a/bot/src/utils/music/Button.ts +++ b/bot/src/utils/music/Button.ts @@ -3,6 +3,7 @@ import { lavalinkManager } from "../../index"; import { checkTextChannelAndMsg, getTextChannelAndMsg } from "./Channel"; import { default_content, default_embed, default_image, getButtons } from "./Config"; import { DB } from "../Database"; +import { Logger } from "../Logger"; export const buttonInteraction = (interaction: ButtonInteraction, args: string[]) => { if (!interaction.guild) return; @@ -16,7 +17,9 @@ export const buttonInteraction = (interaction: ButtonInteraction, args: string[] } else { if (args[0] === "recommend") buttonRecommend(interaction.guild); } - return interaction.deferUpdate().catch(() => {}); + return interaction.deferUpdate().catch((err) => { + Logger.warn(`[Button] deferUpdate 실패: ${String(err)}`); + }); } const buttonRecommend = async (guild: Guild) => { @@ -33,7 +36,7 @@ const buttonRecommend = async (guild: Guild) => { components: [ getButtons() ], files: [ default_image ], }).catch((err) => { - console.error(err); + Logger.error(`[Button] 메세지 수정 실패: ${String(err)}`); return null; }); } \ No newline at end of file diff --git a/bot/src/utils/music/Channel.ts b/bot/src/utils/music/Channel.ts index abbe6de..5e8dced 100644 --- a/bot/src/utils/music/Channel.ts +++ b/bot/src/utils/music/Channel.ts @@ -6,15 +6,19 @@ import { clearAllMsg } from "./Utils"; import { client } from "../../index"; export const getGuildById = async (guildId: string): Promise => { - const guild = client.guilds.cache.get(guildId)?.fetch(); + const guild = await client.guilds.cache.get(guildId)?.fetch().catch(() => undefined); if (!guild) return null; return guild; } export const getMemberById = async (guild: Guild, userId: string): Promise => { - const member = await guild.members.cache.get(userId)?.fetch(true); - if (!member) return null; - return member; + try { + const cached = guild.members.cache.get(userId); + if (!cached) return null; + return await cached.fetch(true); + } catch { + return null; + } } export const getVoiceChannel = (member: GuildMember): VoiceChannel | null => { @@ -25,9 +29,14 @@ export const getVoiceChannel = (member: GuildMember): VoiceChannel | null => { export const getVoiceChannelById = async (guild: Guild, userId: string): Promise => { if (!guild) return null; - const member = await guild.members.cache.get(userId)?.fetch(true); - if (!member) return null; - return getVoiceChannel(member); + try { + const cached = guild.members.cache.get(userId); + if (!cached) return null; + const member = await cached.fetch(true); + return getVoiceChannel(member); + } catch { + return null; + } } export const getTextChannelAndMsg = async (guild: Guild): Promise<{ channel?: TextChannel; msg?: Message; reason?: string; }> => { diff --git a/bot/src/utils/shuffle.ts b/bot/src/utils/shuffle.ts index ce5fe95..afa5b26 100644 --- a/bot/src/utils/shuffle.ts +++ b/bot/src/utils/shuffle.ts @@ -1,15 +1,8 @@ -export const fshuffle = (list: any[]): any[] => { - var i, j, x; - for (i=list.length; i; i-=1) { - j = Math.floor(Math.random()*i); - x = list[i-1]; - list[i-1] = list[j]; - list[j] = x; +/** Fisher-Yates 셔플 (in-place) */ +export const shuffle = (list: T[]): T[] => { + for (let i = list.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [list[i], list[j]] = [list[j], list[i]]; } return list; -} - -export const shuffle = (list: any[]): any[] => { - for (let z=0; z<5; z++) list = fshuffle(list); - return list; } \ No newline at end of file