How to make a Discord bot that pings your Minecraft server🤔
Tags: discord, node, minecraft, typescript
Published: 2023-06-28 01:26:04

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`](https://github.com/woodendoors7/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jk09ydwx079xf3ahkrr5.png)

## 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.
```js
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);
```
### 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
```js
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 })

```
### 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
```js
client.on(Events.InteractionCreate, (interaction: Interaction) => {
    if (!interaction.isChatInputCommand()) return;
    const { commandName } = interaction;
    if (commandName === 'ping') return getMcStatus(interaction);
});
```
### Step 4: Setup MinecraftStatusPinger
As mentioned before, we'll be using the [MinecraftStatusPinger](https://github.com/woodendoors7/MinecraftStatusPinger) library.
Run this command to install it from [NPM](https://www.npmjs.com/minecraftstatuspinger): `npm install minecraftstatuspinger`. This ensures that it is on your computer, and you can import and use it.

Then, import it like so:
```js
import mc from "minecraftstatuspinger"
```
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](https://github.com/woodendoors7/MinecraftStatusPinger#user-content-lookupoptions). This makes the library send a ping request directly to the Minecraft server without contacting any intermediary APIs.
```js
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 })
}
```
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`.
```js
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")
}
```
### 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!
```js
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 ] })
}
```

## Final code
And, we're done! Wasn't so hard, was it? This is how the code should look:
```js
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);

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