refactor(rpc): site↔bot 명령 응답을 SETEX+폴링에서 LPUSH+BRPOP 블로킹으로 전환

- bot: reply() 헬퍼(LPUSH+EXPIRE) 추가, 67개 setex 응답 지점을 일괄 교체.
  결과키를 리스트에 push해 사이트가 BRPOP으로 블로킹 수신하도록 함.
- page(api.ts botRpc): short-polling 제거, 요청마다 전용 커넥션(duplicate)으로
  BRPOP 대기 → 응답 즉시 수신(폴링 지연/헛 GET 제거), 전용 커넥션은 finally에서 정리.
- search route: 사라진 pollIntervalMs 옵션 제거.

검증: 테스트봇(.9)+.7 Redis 실측 BRPOP 왕복 10ms, 고아 키 없음. bot tsc/page next build 통과.
This commit is contained in:
2026-09-12 23:58:52 +09:00
parent 221c31bfa1
commit 31b64b73c5
3 changed files with 102 additions and 116 deletions

View File

@@ -63,56 +63,56 @@ class RedisClientClass {
thumbnail: video.info.artworkUrl ?? "",
duration: video.info.length,
}));
await this.pub.setex(resultKey, 60, JSON.stringify({ spotify, youtubeMusic, youtubeVideo }));
Logger.log(`[Redis Pub] [setex] 결과 저장: (${resultKey})`);
await this.reply(resultKey, 60, JSON.stringify({ spotify, youtubeMusic, youtubeVideo }));
Logger.log(`[Redis Pub] [reply] 결과 저장: (${resultKey})`);
}
if (data.action === "player_play") {
const resultKey = `player:play:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const guild = await getGuildById(data.serverId);
if (!guild) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(guild, data.userId))) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
if (!guild) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(guild, data.userId))) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
let player = lavalinkManager.getPlayer(guild.id);
const voiceChannel = await getVoiceChannelById(guild, data.userId);
if (!player) {
if (!voiceChannel) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
if (!voiceChannel) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
player = (await channelJoin(guild, voiceChannel.id)).player;
} else if (!voiceChannel || voiceChannel.id !== player.voiceChannelId) {
return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 합니다." }));
return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 합니다." }));
}
if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
if (!player) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
await lavalinkManager.search(guild.id, data.track.url, data.userId, player);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "노래 추가 완료" }));
await this.reply(resultKey, 60, JSON.stringify({ success: true, message: "노래 추가 완료" }));
}
if (data.action === "player_playlist") {
const resultKey = `player:playlist:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const guild = await getGuildById(data.serverId);
if (!guild) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(guild, data.userId))) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
if (!guild) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(guild, data.userId))) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
let player = lavalinkManager.getPlayer(guild.id);
const voiceChannel = await getVoiceChannelById(guild, data.userId);
if (!player) {
if (!voiceChannel) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
if (!voiceChannel) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
player = (await channelJoin(guild, voiceChannel.id)).player;
} else if (!voiceChannel || voiceChannel.id !== player.voiceChannelId) {
return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 합니다." }));
return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 합니다." }));
}
if (!player) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
if (!player) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "세션을 찾을수 없습니다." }));
await lavalinkManager.search(guild.id, data.playlistUrl, data.userId, player);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, message: "플레이리스트 추가 완료" }));
await this.reply(resultKey, 60, JSON.stringify({ success: true, message: "플레이리스트 추가 완료" }));
}
if (data.action === "player_now") {
const resultKey = `player:now:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const nowGuild = await getGuildById(data.serverId);
if (!nowGuild) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(nowGuild, data.userId))) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
if (!nowGuild) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(nowGuild, data.userId))) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
const player = lavalinkManager.getPlayer(data.serverId);
await this.pub.setex(resultKey, 60, JSON.stringify({
await this.reply(resultKey, 60, JSON.stringify({
success: true,
botPlayer: !!player,
isPlaying: player?.isPlaying,
@@ -124,19 +124,19 @@ class RedisClientClass {
}
if (data.action === "queue_list") {
const resultKey = `queue:list:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const qlGuild = await getGuildById(data.serverId);
if (!qlGuild) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(qlGuild, data.userId))) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
if (!qlGuild) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
if (!(await this.isMember(qlGuild, data.userId))) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
const player = lavalinkManager.getPlayer(data.serverId);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, queue: player?.queue?.slice(1) ?? [] }));
await this.reply(resultKey, 60, JSON.stringify({ success: true, queue: player?.queue?.slice(1) ?? [] }));
}
if (data.action === "queue_set") {
const resultKey = `queue:set:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.newQueue) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "newQueue를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.newQueue) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "newQueue를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
const nowTrack = context.player.nowTrack ?? context.player.queue?.[0];
@@ -145,85 +145,85 @@ class RedisClientClass {
for (const rawTrack of data.newQueue) {
if (rawTrack.encoded) context.player.queue.push(rawTrack);
}
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
await this.reply(resultKey, 60, JSON.stringify({ success: true }));
context.player.setMsg();
}
if (data.action === "queue_remove") {
const resultKey = `queue:remove:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
// index 는 number(0 도 유효) — typeof 검증으로 변경.
if (typeof data.index !== "number") return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index는 number 이어야 합니다." }));
if (typeof data.index !== "number") return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "index는 number 이어야 합니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
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 (isNaN(numIndex)) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "index 타입이 올바르지 않습니다." }));
if (numIndex < 0) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "index는 0보다 크거나 같아야합니다." }));
// queue[0]은 현재 재생중인 곡이므로 실제 대기열은 queue[1]부터 시작
// numIndex는 대기열(queue[1]~) 기준이므로 실제 splice 위치<EC9C84><ECB998> numIndex+1
if (numIndex >= context.player.queue.length - 1) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "index가 대기열 범위를 초과합니다." }));
if (numIndex >= context.player.queue.length - 1) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "index가 대기열 범위를 초과합니다." }));
// 인덱스 신뢰 대신, 클라이언트가 지우려던 곡(encoded)과 실제 대상이 같은지 확인.
// SSE로 큐가 갱신되는 찰나 인덱스가 밀려 다른 곡이 삭제되는 것을 방지.
const removeTarget = context.player.queue[numIndex + 1];
if (data.encoded && removeTarget?.encoded && removeTarget.encoded !== data.encoded) {
return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "대기열이 변경되었습니다. 새로고침 후 다시 시도해주세요." }));
return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "대기열이 변경되었습니다. 새로고침 후 다시 시도해주세요." }));
}
const [removedTrack] = context.player.queue.splice(numIndex + 1, 1);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, removedTrack }));
await this.reply(resultKey, 60, JSON.stringify({ success: true, removedTrack }));
context.player.setMsg();
}
if (data.action === "player_paused") {
const resultKey = `player:paused:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
// isPaused 는 boolean — false 도 정상 입력. typeof 검증으로 변경.
if (typeof data.isPaused !== "boolean") return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "isPaused는 boolean 이어야 합니다." }));
if (typeof data.isPaused !== "boolean") return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "isPaused는 boolean 이어야 합니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
await context.player.setPause();
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true, paused: context.player.isPaused }));
await this.reply(resultKey, 60, JSON.stringify({ success: true, paused: context.player.isPaused }));
}
if (data.action === "player_skip") {
const resultKey = `player:skip:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (context.player.isPlaying) context.player.skip();
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
await this.reply(resultKey, 60, JSON.stringify({ success: true }));
}
if (data.action === "player_seek") {
const resultKey = `player:seek:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
// seek 는 number(0 도 유효 — 처음으로 되감기) — typeof 검증으로 변경.
if (typeof data.seek !== "number") return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek는 number 이어야 합니다." }));
if (typeof data.seek !== "number") return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "seek는 number 이어야 합니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (!context.player.isPlaying || !context.player.nowTrack) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." }));
if (!context.player.isPlaying || !context.player.nowTrack) return await this.reply(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보다 크거나 같아야합니다." }));
if (numSeek > duration) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "seek가 duration보다 클수 없습니다." }));
if (isNaN(numSeek)) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "seek 타입이 올바르지 않습니다." }));
if (numSeek < 0) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "seek는 0보다 크거나 같아야합니다." }));
if (numSeek > duration) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "seek가 duration보다 클수 없습니다." }));
context.player.seek(numSeek);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
await this.reply(resultKey, 60, JSON.stringify({ success: true }));
}
if (data.action === "player_volume") {
const resultKey = `player:volume:${data.requestId}`;
if (!data.serverId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
if (!data.serverId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "serverId를 찾을수 없습니다." }));
if (!data.userId) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "userId를 찾을수 없습니다." }));
// volume 은 number(0 도 유효 — 음소거) — typeof 검증으로 변경.
if (typeof data.volume !== "number") return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume은 number 이어야 합니다." }));
if (typeof data.volume !== "number") return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "volume은 number 이어야 합니다." }));
const context = await this.getContext(data.serverId, resultKey, data.userId);
if (!context.ok) return;
if (!context.player.isPlaying || !context.player.nowTrack) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "재생중인 노래가 없습니다." }));
if (!context.player.isPlaying || !context.player.nowTrack) return await this.reply(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보다 크거나 같아야합니다." }));
if (numVolume > 100) return await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "volume이 100보다 클수 없습니다." }));
if (isNaN(numVolume)) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "volume 타입이 올바르지 않습니다." }));
if (numVolume < 0) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "volume은 0보다 크거나 같아야합니다." }));
if (numVolume > 100) return await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "volume이 100보다 클수 없습니다." }));
context.player.setVolume(numVolume);
await this.pub.setex(resultKey, 60, JSON.stringify({ success: true }));
await this.reply(resultKey, 60, JSON.stringify({ success: true }));
}
} catch (err) {
Logger.error(`명령어 처리 중 에러: ${String(err)}`);
@@ -238,6 +238,17 @@ class RedisClientClass {
});
}
/**
* 사이트로 보내는 RPC 응답 쓰기.
* 기존 SET+폴링 대신, 사이트가 BRPOP 으로 블로킹 대기하도록 리스트에 LPUSH 한다.
* ttlSec 는 사이트가 이미 타임아웃된 뒤 남는 고아 응답을 자동 정리하기 위한 보험.
* (시그니처를 기존 setex(key, ttl, value) 와 동일하게 맞춰 호출부를 기계적으로 교체)
*/
private async reply(key: string, ttlSec: number, value: string): Promise<void> {
await this.pub.lpush(key, value);
await this.pub.expire(key, ttlSec);
}
public publishState(event: string, data: any) {
const payload = JSON.stringify({
event,
@@ -266,29 +277,29 @@ class RedisClientClass {
} | { ok: false; }> {
const guild = await getGuildById(guildId);
if (!guild) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "guild를 찾을수 없습니다." }));
return { ok: false };
}
// 인가: 요청자가 이 서버의 멤버여야 함 (남의 서버 제어 차단)
if (!(await this.isMember(guild, userId))) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "이 서버의 멤버가 아닙니다." }));
return { ok: false };
}
let player = lavalinkManager.getPlayer(guild.id);
const voiceChannel = await getVoiceChannelById(guild, userId);
if (!player) {
if (!voiceChannel) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "음성채널에 들어가서 이용해주세요." }));
return { ok: false };
}
player = (await channelJoin(guild, voiceChannel.id)).player;
} else if (!voiceChannel || voiceChannel.id !== player.voiceChannelId) {
// 이미 재생 중이면 봇과 같은 음성채널에 있는 사람만 조작 가능
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 조작할 수 있습니다." }));
await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "봇과 같은 음성채널에 있어야 조작할 수 있습니다." }));
return { ok: false };
}
if (!player) {
await this.pub.setex(resultKey, 60, JSON.stringify({ success: false, message: "player를 찾을수 없습니다." }));
await this.reply(resultKey, 60, JSON.stringify({ success: false, message: "player를 찾을수 없습니다." }));
return { ok: false };
}
return {