DEV Community

woodendoors7
woodendoors7

Posted on • Updated on

How to make a Discord bot that pings your Minecraft serveršŸ¤”

Have you ever had your very own Minecraft server and caught yourself thinking, "Wouldn't it be awesome if I could showcase useful information about my server, or track its latency right inside Discord?"

Well, you're in luck! This tutorial will walk you through the entire process, starting from scratch, so you can achieve all of that and more.

We'll be using discord.js, and the MinecraftStatusPinger library, which is a modern, small, performant, zero dependency typescript library written by me that doesn't mess with the server's response.

Discord embed containing info about hypixel

Let's get started!

Step 1: Setup discord.js

This is pretty straightforward. Run npm install discord.js, import discord.js, declare the client, and it's Intents, log in your bot and log a message once the client is ready.

import { Client, Events, EmbedBuilder, REST, SlashCommandBuilder, Routes, GatewayIntentBits, AttachmentBuilder } Ā from 'discord.js';
import mc from "minecraftstatuspinger"

const client = new Client({intents: [GatewayIntentBits.Guilds]});

client.once("ready", c => {
    console.log(`Ready! Logged in as ${c.user.tag}`);
});

// Log in to Discord with your client's token
client.login(process.env.token);
Enter fullscreen mode Exit fullscreen mode

Step 2: Make a slash command.

In this step, we'll make a "ping" slash command. We'll start by declaring REST, which is used for updating slash commands. We set it's token to our token environment variable. We add two options to the command - hostname, and port. We send and register the slash command to Discord, providing your application ID

const rest = new REST({ version: "10" }).setToken(process.env.token)
let commands = [
        new SlashCommandBuilder().setName("ping").setDescription("Pings a Minecraft server.")
            .addStringOption(option =>
                option.setName("ip")
                    .setDescription("IP of server.")
                    .setRequired(true)
            )
            .addIntegerOption(option =>
                option.setName("port")
                    .setDescription("Port of server.")
                    .setRequired(false)
                    .setMinValue(0)
                    .setMaxValue(65535)
            )
    ].map(command => command.toJSON())
   await rest.put(Routes.applicationCommands("app ID"), { body: commands })

Enter fullscreen mode Exit fullscreen mode

Step 3: Handle the slash command.

Subscribe to the InteractionCreate event. When one is received, we check whether it is a slash command interaction. Is it? Bingo. Once that's done, we check it's name, and if it's correct, we call the getMcStatus function, which we create in the next step

client.on(Events.InteractionCreate, (interaction: Interaction) => {
    if (!interaction.isChatInputCommand()) return;
    const { commandName } = interaction;
    if (commandName === 'ping') return getMcStatus(interaction);
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Setup MinecraftStatusPinger

As mentioned before, we'll be using the MinecraftStatusPinger library.
Run this command to install it from NPM: npm install minecraftstatuspinger. This ensures that it is on your computer, and you can import and use it.

Then, import it like so:

import mc from "minecraftstatuspinger"
Enter fullscreen mode Exit fullscreen mode

The setup is quite easier than discord.js! Just install it, import it, and get on your way. No additional setup, no creating a pinger object, it just works out of the box.

Step 5: Ping the Minecraft server

We finally create the getMcStatus function. We need the two options - IP and port. If the port is null, it will automatically switch to 25565 by default. Once we get those, we can call .lookup() with both the options. This makes the library send a ping request directly to the Minecraft server without contacting any intermediary APIs.

async function getMcStatus(interaction){
    let IP = interaction.options.getString("ip")
    let port = interaction.options.getInteger("port") || 25565

    let result = await mc.lookup({ hostname: IP, port: port })
}
Enter fullscreen mode Exit fullscreen mode

now inside status, there is an object with these three children - status, statusRaw, latency.
status is the object which contains the playerlist, version, favicon, and MOTD! We will use all of these in the next step.
statusRaw is just like status, but it is an unparsed string instead of an object.
latency is a number, which represents the latency to the server. It is achieved by sending an arbitrary packet to the server, and waiting for a response, so it's way more accurate than a normal ping.

Step 6: Creating an attachment of the favicon

A minecraft server returns it's favicon as a Data URL blob. We need to remove the prefix, done with the replace function and regex, which replaces all filetypes (like data:image/png;base64, or data:image/jpeg;base64,). Then, it turns it into a buffer, which is that discord.js can use for an image attachment. We set it's name to icon.png.

async function getMcStatus(interaction){
    let IP = interaction.options.getString("ip")
    let port = interaction.options.getInteger("port") || 25565

    let result = await mc.lookup({ hostname: IP, port: port })

    let buf = Buffer.from(result.status.favicon.replace(/data:image\/[^;]+;base64,/, ""), "base64")
    let attach = await new AttachmentBuilder(buf)
Ā  Ā  .setName("icon.png")
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Make an embed and send it

We make a new embed, which we give a title of the IP and the MOTD. The MOTDs vary across versions, for examply hypixel uses result.status.description, modern servers should use result.status.description.text. We add a player count, we add a version field, which shows what version you can join the server from. Lastly, we add a thumbnail, which is the previously defined attachment. Then, we reply to the interaction, and we're done!

async function getMcStatus(interaction){
    let IP = interaction.options.getString("ip")
    let port = interaction.options.getInteger("port") || 25565

    let result = await mc.lookup({ hostname: IP, port: port })

    let buf = Buffer.from(result.status.favicon.replace(/data:image\/[^;]+;base64,/, ""), "base64")
    let attach = await new AttachmentBuilder(buf)
Ā  Ā  .setName("icon.png")
Ā  Ā  
    let motdEmbed = new EmbedBuilder()
        .setTitle(`Status of ${IP}`)
        .setDescription(result.status.description?.textĀ || result.status.description) // MOTD
        .addFields(
            {name: "Players:", value: `${result.status.players.online}/${result.status.players.max}`},
            {name: "Version:", value: result.status.version.name}
        .setThumbnail("attachment://icon.png")
        .setColor("DarkGreen")
    )

    interaction.reply({ embeds: [ motdEmbed ], files: [ attach ] })
}
Enter fullscreen mode Exit fullscreen mode

Final code

And, we're done! Wasn't so hard, was it? This is how the code should look:

import { Client, Events, EmbedBuilder, REST, SlashCommandBuilder, Routes, GatewayIntentBits, AttachmentBuilder }  from 'discord.js';
import mc from "minecraftstatuspinger"

const client = new Client({intents: [GatewayIntentBits.Guilds]});

client.once("ready", c => {
    console.log(`Ready! Logged in as ${c.user.tag}`);
});

client.on(Events.InteractionCreate, (interaction) => {
    if (!interaction.isChatInputCommand()) return;
    const { commandName } = interaction;
    if (commandName === 'ping') return getMcStatus(interaction);
});

async function getMcStatus(interaction){
    let IP = interaction.options.getString("ip")
    let port = interaction.options.getInteger("port") || 25565

    let result = await mc.lookup({hostname: IP, port: port})

    let buf = Buffer.from(result.status.favicon.replace(/data:image\/[^;]+;base64,/, ""), "base64")
    let attach = await new AttachmentBuilder(buf)
    .setName("icon.png")


    let motdEmbed = new EmbedBuilder()
        .setTitle(`Status of ${IP}`)
        .setDescription(result.status.description?.text || result.status.description) // MOTD
        .addFields(
            {name: "Players:", value: `${result.status.players.online}/${result.status.players.max}`},
            {name: "Version:", value: result.status.version.name})
        .setThumbnail("attachment://icon.png")
        .setColor("DarkGreen")

    interaction.reply({ embeds: [ motdEmbed ], files: [ attach ] })
}


const rest = new REST({ version: "10" }).setToken(process.env.token)
let commands = [
        new SlashCommandBuilder().setName("ping").setDescription("Pings a Minecraft server.")
            .addStringOption(option =>
                    option.setName("ip")
                    .setDescription("IP of server.")
                    .setRequired(true)
            )
            .addIntegerOption(option =>
                option.setName("port")
                .setDescription("Port of server.") 
                    .setRequired(false)
                    .setMinValue(0)
                    .setMaxValue(65535)
            )
    ].map(command => command.toJSON())
    rest.put(Routes.applicationCommands("Application ID"), { body: commands })


// Log in to Discord with your client's token
client.login(process.env.token);

Enter fullscreen mode Exit fullscreen mode

Thank you for following this tutorial, I hope everything worked well!

Top comments (0)