Andromeda
Andromeda Blogs

Andromeda Blogs

Discord voice recorder bot

Discord voice recorder bot

Discord.js voice recorder bot

Andromeda's photo
Andromeda
·Mar 2, 2022·

6 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • We need
  • Getting Started
  • Writing a command handler
  • Adding record and play commands
  • Play command

Want to build a voice recorder Discord bot? No problem.

We need

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 installing opusscript with npm 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:

ping-command

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();
 
Share this