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.
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);
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 })
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);
});
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"
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 })
}
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")
}
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 ] })
}
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);
Thank you for following this tutorial, I hope everything worked well!
Top comments (0)