Want to build a voice recorder Discord bot? No problem.
We need
- Node.js JavaScript runtime >=v16.1.x
- discord.js to interact with discord api
- dartjs for simplified voice api
- @discordjs/opus to encode/decode audio packets (required for voice)
- libsodium-wrappers for audio packets encryption (required for voice)
- dotenv to store our secrets
Getting Started
Let's setup our project first. Let's initialize a project with npm init
.
It should create package.json
with similar contents:
{
"name": "voice-recorder-example",
"version": "0.1.0",
"description": "Discord.js voice recorder bot",
"main": "index.js"
...
}
Now, let's install required packages.
$ npm install --save discord.js dartjs @discordjs/opus libsodium-wrappers dotenv
Note: if
@discordjs/opus
fails to install, try installingopusscript
withnpm install opusscript
.
Now, let's setup our bot. First of all, we should make a .env
file and put our bot token there.
# .env source
DISCORD_TOKEN=OTQxMzg5NzU5NTk3Njc0NTQ4.YgVPiA.kvhqAKRWaw26T0FDwVzx-Gnv5E8
Never share your bot token with anyone.
Now, let's create index.js
and write the basic bot.
// load .env contents
require("dotenv").config();
// initialize our bot
const { Client, Intents } = require("discord.js");
const client = new Client({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_VOICE_STATES,
Intents.FLAGS.GUILD_MESSAGES
]
});
client.on("ready", () => {
console.log("Bot is online!");
});
// our command handler will go here
/*
we can just use client.login() without specifying token or process.env.TOKEN
here because we used DISCORD_TOKEN in .env to prefix our token, which
discord.js automatically detects for us.
*/
client.login();
On running the code above, you should see Bot is online!
being printed to the screen.
If you see the errors related to intents, please check out the official discord.js guide related to privileged gateway intents.
Writing a command handler
We need a way to handle commands. When user interacts with the bot, our bot should send appropriate response to the user. There are 2 ways of handling commands:
- new slash commands
- traditional message based commands
For this example, we will use message based commands. Let's start by adding message event listener.
client.on("messageCreate", async (message) => {
// ignore non-guild messages and bots
if (message.author.bot || !message.guildId) return;
if (message.content === "!ping") {
return await message.reply({
embeds: [
{
title: "Pong!",
color: "BLURPLE",
footer: { text: `${Math.floor(client.ws.ping)}ms` }
}
]
});
}
});
Now, we can test the command by sending !ping
as a message. The bot should reply with the following embed:
Now, let's add the audio recorder. For this, we need to import dartjs
and fs
modules.
const { DartVoiceManager } = require("dartjs");
const fs = require("fs");
Then, we have to instantiate DartVoiceManager
:
const voiceManager = new DartVoiceManager(client);
Now, let's add a check to handle recordings dir.
if (!fs.existsSync("./recordings")) fs.mkdirSync("./recordings");
The code should now look similar to this:
require("dotenv").config();
const { Client, Intents } = require("discord.js");
const client = new Client({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_VOICE_STATES,
Intents.FLAGS.GUILD_MESSAGES
]
});
const { DartVoiceManager } = require("dartjs");
const fs = require("fs");
const voiceManager = new DartVoiceManager(client);
if (!fs.existsSync("./recordings")) fs.mkdirSync("./recordings");
client.on("ready", () => {
console.log("Bot is online!");
});
client.on("messageCreate", async (message) => {
if (message.author.bot || !message.guildId) return;
if (message.content === "!ping") {
return await message.reply({
embeds: [
{
title: "Pong!",
color: "BLURPLE",
footer: { text: `${Math.floor(client.ws.ping)}ms` }
}
]
});
}
});
client.login();
Adding record and play commands
Let's start with record command. When a user sends !record
, the bot will join the voice channel user is currently in and record the user. Bot will stop recording once the user stops talking.
if (message.content === "!record") {
// check if user is in voice channel
const vc = message.member.voice.channel;
if (!vc) return await message.reply("You are not in a voice channel!");
// check if bot has permissions to join the voice channel
if (!vc.joinable) return await message.reply("I am unable to join your voice channel!");
// uses DartVoiceManager to join a voice channel
const connection = await voiceManager.join(vc);
// if the client is server deafened, show error message
if (message.guild.me.voice.serverDeaf) {
connection.destroy();
return await message.reply("I am unable to hear you!");
}
// finally add recorder
const receiver = connection.receiver.createStream(message.member, {
mode: "pcm",
end: "silence" // stop recording as soon as user stops talking
});
// save the audio
const writer = receiver.pipe(fs.createWriteStream(`./recordings/${message.author.id}.pcm`));
writer.once("finish", () => {
connection.destroy();
message.reply("Finished writing audio");
});
}
Play command
if (message.content === "!play") {
// check if user is in voice channel
const vc = message.member.voice.channel;
if (!vc) return await message.reply("You are not in a voice channel!");
if (!fs.existsSync(`./recordings/${message.author.id}.pcm`)) return await message.reply("Your audio is not recorded!");
// check if bot has permissions
if (!vc.joinable) return await message.reply("I am unable to join your voice channel!");
if (!vc.speakable) return await message.reply("I am unable to speak in your voice channel!");
// uses DartVoiceManager to join a voice channel
const connection = await voiceManager.join(vc);
// finally play the audio
const stream = fs.createReadStream(`./recordings/${message.author.id}.pcm`);
const dispatcher = connection.play(stream, {
type: "raw"
});
dispatcher.once("finish", () => {
connection.destroy();
return message.channel.send("finished playing audio");
})
}
The final code should look like this:
require("dotenv").config();
const { Client, Intents } = require("discord.js");
const client = new Client({
intents: [
Intents.FLAGS.GUILDS,
Intents.FLAGS.GUILD_VOICE_STATES,
Intents.FLAGS.GUILD_MESSAGES
]
});
const { DartVoiceManager } = require("dartjs");
const fs = require("fs");
const voiceManager = new DartVoiceManager(client);
if (!fs.existsSync("./recordings")) fs.mkdirSync("./recordings");
client.on("ready", () => {
console.log("Bot is online!");
});
client.on("messageCreate", async (message) => {
if (message.author.bot || !message.guildId) return;
if (message.content === "!ping") {
return await message.reply({
embeds: [
{
title: "Pong!",
color: "BLURPLE",
footer: { text: `${Math.floor(client.ws.ping)}ms` }
}
]
});
}
if (message.content === "!record") {
// check if user is in voice channel
const vc = message.member.voice.channel;
if (!vc) return await message.reply("You are not in a voice channel!");
// check if bot has permissions to join the voice channel
if (!vc.joinable) return await message.reply("I am unable to join your voice channel!");
// uses DartVoiceManager to join a voice channel
const connection = await voiceManager.join(vc);
// if the client is server deafened, show error message
if (message.guild.me.voice.serverDeaf) {
connection.destroy();
return await message.reply("I am unable to hear you!");
}
// finally add recorder
const receiver = connection.receiver.createStream(message.member, {
mode: "pcm",
end: "silence" // stop recording as soon as user stops talking
});
// save the audio
const writer = receiver.pipe(fs.createWriteStream(`./recordings/${message.author.id}.pcm`));
writer.once("finish", () => {
connection.destroy();
message.reply("Finished writing audio");
});
}
if (message.content === "!play") {
// check if user is in voice channel
const vc = message.member.voice.channel;
if (!vc) return await message.reply("You are not in a voice channel!");
if (!fs.existsSync(`./recordings/${message.author.id}.pcm`)) return await message.reply("Your audio is not recorded!");
// check if bot has permissions
if (!vc.joinable) return await message.reply("I am unable to join your voice channel!");
if (!vc.speakable) return await message.reply("I am unable to speak in your voice channel!");
// uses DartVoiceManager to join a voice channel
const connection = await voiceManager.join(vc);
// finally play the audio
const stream = fs.createReadStream(`./recordings/${message.author.id}.pcm`);
const dispatcher = connection.play(stream, {
type: "raw"
});
dispatcher.once("finish", () => {
connection.destroy();
return message.channel.send("finished playing audio");
})
}
});
client.login();