Voice settings
So far the bot joins a channel when someone runs /play. In this guide you add four commands that control the voice
connection directly:
/joinconnects to the member’s channel, or brings the bot and whatever it is playing there./leavedisconnects./deafenand/mutetoggle the bot’s self-deafen and self-mute.
distube.voices holds one voice per guild, with or without a queue. None of these commands needs a queue.
Create commands/join.ts:
import { type ChatInputCommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js";import type { DisTube } from "distube";import type { CommandMetadata } from "./index.ts";
export const data = new SlashCommandBuilder() .setName("join") .setDescription("Join your voice channel without playing anything") .setContexts(InteractionContextType.Guild) .toJSON();
export async function execute( interaction: ChatInputCommandInteraction<"cached">, distube: DisTube<CommandMetadata>,): Promise<void> { const channel = interaction.member.voice.channel; if (!channel) { await interaction.reply("Join a voice channel first."); return; } await interaction.deferReply(); await distube.voices.join(channel, { selfDeaf: false }); await interaction.editReply(`Joined ${channel.name}.`);}voices.join resolves the voice once the bot is connected. The bot then stays in the channel until something makes
it leave, as Leave voice explains.
If the bot is already in another channel of the server, join moves it to the member’s channel. The queue, the
volume and the filters move with it, and playback continues in the new channel. That makes a separate move command
unnecessary.
The bot deafens itself by default whenever it joins, from /play or from voices.join. This command passes
selfDeaf: false to join undeafened, so a /join that moves a deafened bot also undeafens it. Leave the option out
to keep the default.
Create commands/leave.ts:
import { type ChatInputCommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js";import type { DisTube } from "distube";import type { CommandMetadata } from "./index.ts";
export const data = new SlashCommandBuilder() .setName("leave") .setDescription("Leave the voice channel") .setContexts(InteractionContextType.Guild) .toJSON();
export async function execute( interaction: ChatInputCommandInteraction<"cached">, distube: DisTube<CommandMetadata>,): Promise<void> { if (!distube.voices.has(interaction.guildId)) { await interaction.reply("I am not connected to a voice channel."); return; } await interaction.deferReply(); await distube.voices.leave(interaction.guildId); await interaction.editReply("Left the voice channel.");}After a /leave, the guild has no voice. A second /leave finds nothing and replies that the bot is not connected.
Deafen and mute
Section titled “Deafen and mute”Create commands/deafen.ts:
import { type ChatInputCommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js";import type { DisTube } from "distube";import type { CommandMetadata } from "./index.ts";
export const data = new SlashCommandBuilder() .setName("deafen") .setDescription("Toggle whether the bot deafens itself") .setContexts(InteractionContextType.Guild) .addBooleanOption(option => option.setName("enabled").setDescription("Explicit state instead of toggling")) .toJSON();
export async function execute( interaction: ChatInputCommandInteraction<"cached">, distube: DisTube<CommandMetadata>,): Promise<void> { const voice = distube.voices.get(interaction.guildId); if (!voice) { await interaction.reply("I am not connected to a voice channel."); return; } const enabled = interaction.options.getBoolean("enabled") ?? !voice.selfDeaf; await interaction.deferReply(); await voice.setSelfDeaf(enabled); await interaction.editReply(enabled ? "Self-deafened." : "Undeafened.");}Create commands/mute.ts:
import { type ChatInputCommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js";import type { DisTube } from "distube";import type { CommandMetadata } from "./index.ts";
export const data = new SlashCommandBuilder() .setName("mute") .setDescription("Toggle whether the bot mutes itself") .setContexts(InteractionContextType.Guild) .addBooleanOption(option => option.setName("enabled").setDescription("Explicit state instead of toggling")) .toJSON();
export async function execute( interaction: ChatInputCommandInteraction<"cached">, distube: DisTube<CommandMetadata>,): Promise<void> { const voice = distube.voices.get(interaction.guildId); if (!voice) { await interaction.reply("I am not connected to a voice channel."); return; } const enabled = interaction.options.getBoolean("enabled") ?? !voice.selfMute; await interaction.deferReply(); await voice.setSelfMute(enabled); await interaction.editReply(enabled ? "Self-muted." : "Unmuted.");}Both commands take an optional enabled option. Without it, they flip the current state, which they read from
voice.selfDeaf and voice.selfMute. Like volume and filters, these settings stay with the voice when a queue ends.
Register the commands
Section titled “Register the commands”In commands/index.ts, add one import per file:
import * as deafen from "./deafen.ts";import * as join from "./join.ts";import * as leave from "./leave.ts";import * as mute from "./mute.ts";Then put join, leave, deafen and mute in the commands list.
The voice and filters reference lists every voice setting. The example bot has the same leave.ts, deafen.ts and mute.ts.
Same channel check stops members outside the bot’s channel from controlling it.