Fix multi channel happenings
This commit is contained in:
@@ -18,45 +18,64 @@ export interface AudioServiceOptions {
|
||||
tts: TTSRegistry;
|
||||
}
|
||||
|
||||
const SUMMON_LOCK_MS = 60_000;
|
||||
|
||||
export interface MoveOptions {
|
||||
summon?: boolean;
|
||||
}
|
||||
|
||||
export class AudioService {
|
||||
readonly player: AudioPlayer = createAudioPlayer();
|
||||
readonly queue: AudioQueue = new AudioQueue(this.player);
|
||||
|
||||
private joinedChannels: VoiceBasedChannel[] = [];
|
||||
private connections = new Map<VoiceBasedChannel, VoiceConnection>();
|
||||
private currentChannel: VoiceBasedChannel | undefined;
|
||||
private currentConnection: VoiceConnection | undefined;
|
||||
private summonLockUntil = 0;
|
||||
|
||||
constructor(private readonly opts: AudioServiceOptions) {}
|
||||
|
||||
getActiveVoiceChannel(): VoiceBasedChannel | undefined {
|
||||
return this.joinedChannels[0];
|
||||
getCurrentChannel(): VoiceBasedChannel | undefined {
|
||||
return this.currentChannel;
|
||||
}
|
||||
|
||||
isInVoiceChannel(channel: VoiceBasedChannel): boolean {
|
||||
return this.joinedChannels.includes(channel);
|
||||
getCurrentConnection(): VoiceConnection | undefined {
|
||||
return this.currentConnection;
|
||||
}
|
||||
|
||||
getConnectionForVoiceChannel(channel: VoiceBasedChannel): VoiceConnection | undefined {
|
||||
return this.connections.get(channel);
|
||||
isInChannel(channel: VoiceBasedChannel): boolean {
|
||||
return this.currentChannel?.id === channel.id;
|
||||
}
|
||||
|
||||
async joinChannel(channel: VoiceBasedChannel): Promise<void> {
|
||||
if (this.isInVoiceChannel(channel)) return;
|
||||
isSummonLocked(): boolean {
|
||||
if (this.summonLockUntil <= Date.now()) return false;
|
||||
// Release the lock early if the summon channel has only the bot left.
|
||||
if ((this.currentChannel?.members.size ?? 0) < 2) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async moveTo(channel: VoiceBasedChannel, opts: MoveOptions = {}): Promise<void> {
|
||||
if (opts.summon) this.summonLockUntil = Date.now() + SUMMON_LOCK_MS;
|
||||
if (this.currentChannel?.id === channel.id) return;
|
||||
|
||||
const connection = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: createAdapter(channel),
|
||||
});
|
||||
connection.subscribe(this.player);
|
||||
this.joinedChannels.push(channel);
|
||||
this.connections.set(channel, connection);
|
||||
|
||||
this.currentConnection?.destroy();
|
||||
this.currentChannel = channel;
|
||||
this.currentConnection = connection;
|
||||
}
|
||||
|
||||
async leaveChannel(channel: VoiceBasedChannel): Promise<void> {
|
||||
if (!this.isInVoiceChannel(channel)) return;
|
||||
const conn = this.connections.get(channel);
|
||||
this.joinedChannels = this.joinedChannels.filter((c) => c !== channel);
|
||||
conn?.disconnect();
|
||||
this.connections.delete(channel);
|
||||
async leave(): Promise<void> {
|
||||
if (!this.currentConnection) return;
|
||||
this.queue.flush();
|
||||
this.currentConnection.destroy();
|
||||
this.currentConnection = undefined;
|
||||
this.currentChannel = undefined;
|
||||
this.summonLockUntil = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,67 @@
|
||||
import type { Guild, VoiceBasedChannel } from "discord.js";
|
||||
import { ChannelType } from "discord.js";
|
||||
import type { Module } from "./types.js";
|
||||
|
||||
function chooseTargetChannel(
|
||||
guild: Guild,
|
||||
recentEventChannel: VoiceBasedChannel | null | undefined,
|
||||
currentChannelId: string | undefined,
|
||||
): VoiceBasedChannel | undefined {
|
||||
let best: VoiceBasedChannel | undefined;
|
||||
let bestCount = 0;
|
||||
|
||||
for (const ch of guild.channels.cache.values()) {
|
||||
if (ch.type !== ChannelType.GuildVoice && ch.type !== ChannelType.GuildStageVoice) continue;
|
||||
const voice = ch as VoiceBasedChannel;
|
||||
const humans = voice.members.filter((m) => !m.user.bot).size;
|
||||
if (humans === 0) continue;
|
||||
|
||||
if (humans > bestCount) {
|
||||
best = voice;
|
||||
bestCount = humans;
|
||||
continue;
|
||||
}
|
||||
if (humans === bestCount && best) {
|
||||
// Tie-break: prefer the channel that just had activity, then the bot's current channel.
|
||||
if (recentEventChannel && voice.id === recentEventChannel.id) {
|
||||
best = voice;
|
||||
} else if (
|
||||
currentChannelId &&
|
||||
voice.id === currentChannelId &&
|
||||
best.id !== recentEventChannel?.id
|
||||
) {
|
||||
best = voice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
export const announcer: Module = ({ client, audio, tts, t }) => {
|
||||
client.on("voiceStateUpdate", async (oldState, newState) => {
|
||||
if (newState.member?.user.bot) return;
|
||||
if (oldState.channel && newState.channel) return;
|
||||
const channel = oldState.channel ?? newState.channel;
|
||||
if (!channel) return;
|
||||
|
||||
const joined = !oldState.channel;
|
||||
const guild = newState.guild ?? oldState.guild;
|
||||
const recentChannel = newState.channel ?? oldState.channel;
|
||||
|
||||
if (!joined && channel.members.size < 2) {
|
||||
audio.queue.flush();
|
||||
await audio.leaveChannel(channel);
|
||||
return;
|
||||
if (!audio.isSummonLocked()) {
|
||||
const target = chooseTargetChannel(guild, recentChannel, audio.getCurrentChannel()?.id);
|
||||
const current = audio.getCurrentChannel();
|
||||
if (!target && current) {
|
||||
await audio.leave();
|
||||
} else if (target && target.id !== current?.id) {
|
||||
await audio.moveTo(target);
|
||||
}
|
||||
}
|
||||
|
||||
await audio.joinChannel(channel);
|
||||
const joined = !oldState.channel && !!newState.channel;
|
||||
const left = !!oldState.channel && !newState.channel;
|
||||
if (!joined && !left) return;
|
||||
|
||||
const eventChannel = newState.channel ?? oldState.channel;
|
||||
const current = audio.getCurrentChannel();
|
||||
if (!eventChannel || !current || eventChannel.id !== current.id) return;
|
||||
|
||||
const username = newState.member?.displayName ?? oldState.member?.displayName ?? "someone";
|
||||
const str = joined ? t("USER_JOINED", username) : t("USER_LEFT", username);
|
||||
|
||||
@@ -8,7 +8,7 @@ export const summon: Module = ({ audio, commands, rootDir }) => {
|
||||
commands.register("summon", async (_args, message) => {
|
||||
const channel = message.member?.voice.channel;
|
||||
if (!channel) return;
|
||||
await audio.joinChannel(channel);
|
||||
await audio.moveTo(channel, { summon: true });
|
||||
respond(audio, sysmsg, message, "Hi!");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export const welcomer: Module = ({ client, audio, config, strings, rootDir }) =>
|
||||
return;
|
||||
}
|
||||
const voiceChannel = channel as VoiceBasedChannel;
|
||||
await audio.joinChannel(voiceChannel);
|
||||
await audio.moveTo(voiceChannel, { summon: true });
|
||||
audio.queue.add(sysstart);
|
||||
await audio.speak(voiceChannel, strings.WELCOME);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user