431 lines
18 KiB
JavaScript
431 lines
18 KiB
JavaScript
import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes, EmbedBuilder } from 'discord.js';
|
|
import jsonfile from 'jsonfile';
|
|
import MyMCLib from 'mymc-lib';
|
|
import unirest from 'unirest';
|
|
import { readFileSync } from 'fs';
|
|
import cmd from 'cmd-promise';
|
|
|
|
// Paths to config and tokens files
|
|
const tokensFile = './tokens.json';
|
|
const config = JSON.parse(readFileSync('./config.json', 'utf8'));
|
|
|
|
// Initialize Discord client
|
|
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
|
|
|
// Load tokens from the JSON file
|
|
function loadTokens() {
|
|
try {
|
|
return jsonfile.readFileSync(tokensFile);
|
|
} catch (error) {
|
|
console.error('Error reading tokens file:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Save tokens to the JSON file
|
|
function saveTokens(tokens) {
|
|
jsonfile.writeFileSync(tokensFile, tokens, { spaces: 2 });
|
|
}
|
|
|
|
// Automatically request a new token if it doesn't exist or is invalid
|
|
async function fetchAndSaveToken(userId, interaction) {
|
|
return unirest
|
|
.post(config.endpoint.toString())
|
|
.headers({ 'Accept': 'application/json', 'Content-Type': 'application/json' })
|
|
.send({ "username": `mc_${userId}`, "password": config.password.toString()})
|
|
.then((tokenInfo) => {
|
|
const tokens = loadTokens();
|
|
tokens[userId] = tokenInfo.body.token; // Save the new token
|
|
saveTokens(tokens);
|
|
return tokenInfo.body.token;
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error fetching token:', error);
|
|
sendSexyEmbed("Error", "An error occurred while fetching your API token.", interaction);
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
// Fetch or retrieve token, if the token is invalid, fetch a new one
|
|
async function getToken(userId, interaction) {
|
|
const tokens = loadTokens();
|
|
if (!tokens[userId]) {
|
|
return await fetchAndSaveToken(userId, interaction);
|
|
}
|
|
return tokens[userId];
|
|
}
|
|
|
|
// Handle invalid tokens and re-fetch them
|
|
async function handleApiCall(apiCall, userId, interaction) {
|
|
try {
|
|
return await apiCall();
|
|
} catch (error) {
|
|
console.error('Token error, re-fetching token...');
|
|
await fetchAndSaveToken(userId, interaction);
|
|
return await apiCall();
|
|
}
|
|
}
|
|
|
|
// Handle multiple fields or single-field responses
|
|
function handleResponse(response, interaction, ephemeral = false) {
|
|
if (response.success && typeof response === 'object' && Object.keys(response).length > 1) {
|
|
// Extract the message if it exists, otherwise use a default description
|
|
const description = response.message ? response.message : 'Success!';
|
|
|
|
// If there is a 'stats' field, handle it separately to format it correctly
|
|
if (response.stats) {
|
|
const statsFields = [];
|
|
|
|
// Handle each field in the stats object
|
|
Object.entries(response.stats).forEach(([key, value]) => {
|
|
if (key === 'memory') {
|
|
// Format memory separately to display raw and percent nicely
|
|
statsFields.push({
|
|
name: 'Memory Usage',
|
|
value: `Raw: ${value.raw}\nPercent: ${value.percent}`,
|
|
inline: false
|
|
});
|
|
} else {
|
|
statsFields.push({
|
|
name: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the key name
|
|
value: String(value),
|
|
inline: false
|
|
});
|
|
}
|
|
});
|
|
|
|
sendSexyEmbedWithFields('My-MC Link - Server Stats', description, statsFields, interaction, ephemeral);
|
|
}
|
|
// If there is a 'mods' field, handle it separately to format it correctly
|
|
else if (Array.isArray(response.mods)) {
|
|
const modFields = response.mods.map((mod, index) => ({
|
|
name: `Mod ${index + 1}: ${mod.name}`,
|
|
value: `Version: ${mod.version}\nSource: ${mod.source}\nEssential: ${mod.essential ? 'Yes' : 'No'}`,
|
|
inline: false
|
|
}));
|
|
|
|
sendSexyEmbedWithFields('My-MC Link - Installed Mods', description, modFields, interaction, ephemeral);
|
|
}
|
|
// Handle all other fields
|
|
else {
|
|
const fields = Object.entries(response)
|
|
.filter(([key]) => key !== 'success' && key !== 'action' && key !== 'message' && key !== 'stats' && key !== 'mods') // Ignore specific fields
|
|
.map(([key, value]) => ({ name: key, value: String(value) }));
|
|
|
|
sendSexyEmbedWithFields('My-MC Link', description, fields, interaction, ephemeral);
|
|
}
|
|
} else {
|
|
// Single message or field
|
|
sendSexyEmbed('Response', JSON.stringify(response, null, 2), interaction, ephemeral);
|
|
}
|
|
}
|
|
|
|
|
|
// Send sexy embed
|
|
function sendSexyEmbed(title, description, interaction, ephemeral = false) {
|
|
const embed = new EmbedBuilder()
|
|
.setColor("#3498DB")
|
|
.setTitle(title)
|
|
.setDescription(description)
|
|
.setTimestamp()
|
|
.setFooter({
|
|
text: `Requested by ${interaction.user.username}`,
|
|
iconURL: `${interaction.user.displayAvatarURL()}`
|
|
});
|
|
interaction.reply({
|
|
embeds: [embed],
|
|
ephemeral: ephemeral // Set ephemeral flag based on the condition
|
|
});
|
|
}
|
|
|
|
// Send sexy embed with fields
|
|
function sendSexyEmbedWithFields(title, description, fields, interaction, ephemeral = false) {
|
|
const embed = new EmbedBuilder()
|
|
.setColor("#3498DB")
|
|
.setTitle(title)
|
|
.setDescription(description !== "N/A" ? description : undefined)
|
|
.addFields(fields)
|
|
.setTimestamp()
|
|
.setFooter({
|
|
text: `Requested by ${interaction.user.username}`,
|
|
iconURL: `${interaction.user.displayAvatarURL()}`
|
|
});
|
|
interaction.reply({
|
|
embeds: [embed],
|
|
ephemeral: ephemeral // Set ephemeral flag based on the condition
|
|
});
|
|
}
|
|
|
|
// Slash command definitions
|
|
const commands = [
|
|
// new SlashCommandBuilder().setName('api-key-time').setDescription('Get the server time'),
|
|
new SlashCommandBuilder().setName('stats').setDescription('Get the server statistics'),
|
|
new SlashCommandBuilder().setName('logs').setDescription('Get the server log'),
|
|
new SlashCommandBuilder().setName('start-server').setDescription('Start the Minecraft server'),
|
|
new SlashCommandBuilder().setName('stop-server').setDescription('Stop the Minecraft server'),
|
|
new SlashCommandBuilder().setName('restart-server').setDescription('Restart the Minecraft server'),
|
|
new SlashCommandBuilder().setName('my-mc-link').setDescription('Create a custom server link'),
|
|
new SlashCommandBuilder().setName('my-sftp-link').setDescription('Create an SFTP link'),
|
|
new SlashCommandBuilder().setName('get-connection-hash').setDescription('Get the connection hash'),
|
|
new SlashCommandBuilder().setName('get-connection-hash-sftp').setDescription('Get the SFTP connection hash'),
|
|
new SlashCommandBuilder().setName('list-players').setDescription('Get a list of online players'),
|
|
new SlashCommandBuilder().setName('my-website-url').setDescription('Get the server website URL'),
|
|
new SlashCommandBuilder().setName('bluemap-url ').setDescription('Get the server map URL'),
|
|
new SlashCommandBuilder().setName('ban').setDescription('Ban a player by username').addStringOption(option => option.setName('username').setDescription('Player username').setRequired(true)),
|
|
new SlashCommandBuilder().setName('unban').setDescription('Unban a player by username').addStringOption(option => option.setName('username').setDescription('Player username').setRequired(true)),
|
|
new SlashCommandBuilder().setName('send-message').setDescription('Send a message to all players').addStringOption(option => option.setName('message').setDescription('Message to send').setRequired(true)),
|
|
new SlashCommandBuilder().setName('send-private-message').setDescription('Send a private message to a player').addStringOption(option => option.setName('username').setDescription('Player username').setRequired(true)).addStringOption(option => option.setName('message').setDescription('Message to send').setRequired(true)),
|
|
new SlashCommandBuilder().setName('mc-cmd').setDescription('Execute a command in the server console').addStringOption(option => option.setName('command').setDescription('Command to execute').setRequired(true)),
|
|
new SlashCommandBuilder().setName('give').setDescription('Give an item to a player').addStringOption(option => option.setName('username').setDescription('Player username').setRequired(true)).addStringOption(option => option.setName('item').setDescription('Item to give').setRequired(true)).addIntegerOption(option => option.setName('amount').setDescription('Amount to give').setRequired(true)),
|
|
new SlashCommandBuilder().setName('install-mod').setDescription('Install a mod by ID').addStringOption(option => option.setName('modid').setDescription('Mod ID').setRequired(true)),
|
|
new SlashCommandBuilder().setName('uninstall-mod').setDescription('Uninstall a mod by ID').addStringOption(option => option.setName('modid').setDescription('Mod ID').setRequired(true)),
|
|
new SlashCommandBuilder().setName('mod-list').setDescription('Get a list of installed mods'),
|
|
];
|
|
|
|
// Prepare extra data for user commands
|
|
const JSONCommands = commands.map(command => command.toJSON());
|
|
const extras = {
|
|
"integration_types": [0, 1], // 0 for guild, 1 for user
|
|
"contexts": [0, 1, 2], // 0 for guild, 1 for app DMs, 2 for GDMs and other DMs
|
|
};
|
|
|
|
JSONCommands.forEach((command) => {
|
|
Object.keys(extras).forEach(key => command[key] = extras[key]);
|
|
});
|
|
|
|
// Register commands with Discord
|
|
const rest = new REST({ version: '10' }).setToken(config.token);
|
|
(async () => {
|
|
try {
|
|
console.log('Started refreshing application (/) commands.');
|
|
|
|
await rest.put(
|
|
Routes.applicationCommands(config.clientId),
|
|
{ body: JSONCommands } // Add all commands as an array
|
|
);
|
|
|
|
console.log('Successfully reloaded application (/) commands.');
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
})();
|
|
|
|
// Handle bot interactions
|
|
client.on('interactionCreate', async interaction => {
|
|
if (!interaction.isCommand()) return;
|
|
|
|
const userId = interaction.user.id;
|
|
|
|
// Fetch or get existing token
|
|
const apiToken = await getToken(userId, interaction);
|
|
|
|
const MyMC = new MyMCLib(apiToken);
|
|
|
|
switch (interaction.commandName) {
|
|
// case 'server-time':
|
|
// const timeData = await handleApiCall(() => MyMC.getTime(), userId, interaction);
|
|
// handleResponse(timeData, interaction);
|
|
// break;
|
|
|
|
case 'stats':
|
|
const stats = await handleApiCall(() => MyMC.getStats(), userId, interaction);
|
|
handleResponse(stats, interaction);
|
|
break;
|
|
|
|
case 'logs':
|
|
const log = await handleApiCall(() => MyMC.getLog(), userId, interaction);
|
|
handleResponse(log, interaction);
|
|
break;
|
|
|
|
case 'start-server':
|
|
const startResult = await handleApiCall(() => MyMC.startServer(), userId, interaction);
|
|
handleResponse(startResult, interaction);
|
|
break;
|
|
|
|
case 'stop-server':
|
|
const stopResult = await handleApiCall(() => MyMC.stopServer(), userId, interaction);
|
|
handleResponse(stopResult, interaction);
|
|
break;
|
|
|
|
case 'restart-server':
|
|
const restartResult = await handleApiCall(() => MyMC.restartServer(), userId, interaction);
|
|
handleResponse(restartResult, interaction);
|
|
break;
|
|
|
|
case 'my-mc-link':
|
|
try {
|
|
// Check if the server is running
|
|
const runningCheck = await cmd(`node /home/mchost/scripts/docker_exec.js mc_${interaction.user.id} "/" "echo test"`);
|
|
console.log(runningCheck.stdout);
|
|
|
|
if (runningCheck.stdout.includes("not running")) {
|
|
const response = {
|
|
success: false
|
|
};
|
|
return sendSexyEmbed("Server Booted", "Please use /start-server to boot the server.", interaction)
|
|
}
|
|
|
|
// Check if the server is online
|
|
const out = await cmd(`sh /home/mchost/scripts/check_online.sh mc_${interaction.user.id}`);
|
|
console.log(out.stdout);
|
|
|
|
// Assuming out.stdout is expected to be a string; '0' indicates still booting
|
|
if (out.stdout.trim() === '0') {
|
|
const response = {
|
|
success: false
|
|
};
|
|
return sendSexyEmbed("Still Booting", "Please wait one minute and try again.", interaction)
|
|
}
|
|
|
|
// Create custom link if checks pass
|
|
const customLinkResult = await handleApiCall(() => MyMC.createMyLink(), userId, interaction);
|
|
handleResponse(customLinkResult, interaction);
|
|
|
|
} catch (error) {
|
|
console.error('Error during my-mc-link command:', error);
|
|
const response = {
|
|
success: false,
|
|
fields: [
|
|
{ name: "Error", value: "An error occurred while processing your request." },
|
|
{ name: "Suggestion", value: "Please try again later." }
|
|
]
|
|
};
|
|
handleResponse(response, interaction);
|
|
}
|
|
break;
|
|
|
|
|
|
|
|
case 'my-mc-link':
|
|
try {
|
|
// Check if the server is running
|
|
const runningCheck = await cmd(`node /home/mchost/scripts/docker_exec.js mc_${interaction.user.id} "/" "echo test"`);
|
|
console.log(runningCheck.stdout);
|
|
|
|
if (runningCheck.stdout.includes("not running")) {
|
|
const response = {
|
|
success: false
|
|
};
|
|
return sendSexyEmbed("Server Booted", "Please use /start-server to boot the server.", interaction)
|
|
}
|
|
|
|
// Check if the server is online
|
|
const out = await cmd(`sh /home/mchost/scripts/check_online.sh mc_${interaction.user.id}`);
|
|
console.log(out.stdout);
|
|
|
|
// Assuming out.stdout is expected to be a string; '0' indicates still booting
|
|
if (out.stdout.trim() === '0') {
|
|
const response = {
|
|
success: false
|
|
};
|
|
return sendSexyEmbed("Still Booting", "Please wait one minute and try again.", interaction)
|
|
}
|
|
|
|
// Create SFTP link if checks pass
|
|
const sftpLinkResult = await handleApiCall(() => MyMC.createLinkSFTP(), userId, interaction);
|
|
handleResponse(sftpLinkResult, interaction, true);
|
|
|
|
} catch (error) {
|
|
console.error('Error during my-mc-link command:', error);
|
|
const response = {
|
|
success: false,
|
|
fields: [
|
|
{ name: "Error", value: "An error occurred while processing your request." },
|
|
{ name: "Suggestion", value: "Please try again later." }
|
|
]
|
|
};
|
|
handleResponse(response, interaction, true);
|
|
}
|
|
break;
|
|
|
|
|
|
case 'get-connection-hash':
|
|
const hash = await handleApiCall(() => MyMC.getConnectionHash(), userId, interaction);
|
|
handleResponse(hash, interaction, true);
|
|
break;
|
|
|
|
case 'get-connection-hash-sftp':
|
|
const sftpHash = await handleApiCall(() => MyMC.getConnectionHashSFTP(), userId, interaction);
|
|
handleResponse(sftpHash, interaction, true);
|
|
break;
|
|
|
|
case 'list-players':
|
|
const players = await handleApiCall(() => MyMC.getOnlinePlayers(), userId, interaction);
|
|
handleResponse(players, interaction);
|
|
break;
|
|
|
|
case 'my-website-url':
|
|
const websiteURL = await handleApiCall(() => MyMC.getWebsiteURL(), userId, interaction);
|
|
handleResponse(websiteURL, interaction);
|
|
break;
|
|
|
|
case 'bluemap-url ':
|
|
const mapURL = await handleApiCall(() => MyMC.getMapURL(), userId, interaction);
|
|
handleResponse(mapURL, interaction);
|
|
break;
|
|
|
|
case 'ban':
|
|
const banUsername = interaction.options.getString('username');
|
|
const banResult = await handleApiCall(() => MyMC.postBan(banUsername), userId, interaction);
|
|
handleResponse(banResult, interaction, true);
|
|
break;
|
|
|
|
case 'unban':
|
|
const unbanUsername = interaction.options.getString('username');
|
|
const unbanResult = await handleApiCall(() => MyMC.postUnban(unbanUsername), userId, interaction);
|
|
handleResponse(unbanResult, interaction, true);
|
|
break;
|
|
|
|
case 'send-message':
|
|
const message = interaction.options.getString('message');
|
|
const sayResult = await handleApiCall(() => MyMC.postSay(message), userId, interaction);
|
|
handleResponse(sayResult, interaction, true);
|
|
break;
|
|
|
|
case 'send-private-message':
|
|
const tellUsername = interaction.options.getString('username');
|
|
const privateMessage = interaction.options.getString('message');
|
|
const tellResult = await handleApiCall(() => MyMC.postTell(tellUsername, privateMessage), userId, interaction);
|
|
handleResponse(tellResult, interaction, true);
|
|
break;
|
|
|
|
case 'mc-cmd':
|
|
const command = interaction.options.getString('command');
|
|
const consoleResult = await handleApiCall(() => MyMC.postConsole(command), userId, interaction);
|
|
handleResponse(consoleResult, interaction, true);
|
|
break;
|
|
|
|
case 'give':
|
|
const giveUsername = interaction.options.getString('username');
|
|
const item = interaction.options.getString('item');
|
|
const amount = interaction.options.getInteger('amount');
|
|
const giveResult = await handleApiCall(() => MyMC.postGive(giveUsername, item, amount), userId, interaction);
|
|
handleResponse(giveResult, interaction);
|
|
break;
|
|
|
|
case 'install-mod':
|
|
const modId = interaction.options.getString('modid');
|
|
const installResult = await handleApiCall(() => MyMC.installMod(modId), userId, interaction);
|
|
handleResponse(installResult, interaction);
|
|
break;
|
|
|
|
case 'uninstall-mod':
|
|
const uninstallModId = interaction.options.getString('modid');
|
|
const uninstallResult = await handleApiCall(() => MyMC.uninstallMod(uninstallModId), userId, interaction);
|
|
handleResponse(uninstallResult, interaction);
|
|
break;
|
|
|
|
case 'mod-list':
|
|
const installedMods = await handleApiCall(() => MyMC.getInstalledMods(), userId, interaction);
|
|
handleResponse(installedMods, interaction);
|
|
break;
|
|
|
|
default:
|
|
await interaction.reply('Command not recognized.');
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Log in to Discord
|
|
client.login(config.token);
|