hypermc-api-user-install-bot/public_bot.js

431 lines
18 KiB
JavaScript
Raw Permalink Normal View History

2024-10-01 23:31:27 -04:00
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';
2024-10-02 00:06:33 -04:00
import cmd from 'cmd-promise';
2024-10-01 23:31:27 -04:00
// 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
2024-10-02 00:06:33 -04:00
.post(config.endpoint.toString())
2024-10-01 23:31:27 -04:00
.headers({ 'Accept': 'application/json', 'Content-Type': 'application/json' })
2024-10-02 00:06:33 -04:00
.send({ "username": `mc_${userId}`, "password": config.password.toString()})
2024-10-01 23:31:27 -04:00
.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
2024-10-01 23:38:26 -04:00
const description = response.message ? response.message : 'Success!';
2024-10-01 23:31:27 -04:00
// 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 = [
2024-10-01 23:38:26 -04:00
// new SlashCommandBuilder().setName('api-key-time').setDescription('Get the server time'),
2024-10-02 02:27:21 -04:00
new SlashCommandBuilder().setName('stats').setDescription('Get the server statistics'),
new SlashCommandBuilder().setName('logs').setDescription('Get the server log'),
2024-10-01 23:31:27 -04:00
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'),
2024-10-02 02:27:21 -04:00
new SlashCommandBuilder().setName('my-mc-link').setDescription('Create a custom server link'),
new SlashCommandBuilder().setName('my-sftp-link').setDescription('Create an SFTP link'),
2024-10-01 23:31:27 -04:00
new SlashCommandBuilder().setName('get-connection-hash').setDescription('Get the connection hash'),
new SlashCommandBuilder().setName('get-connection-hash-sftp').setDescription('Get the SFTP connection hash'),
2024-10-02 02:27:21 -04:00
new SlashCommandBuilder().setName('list-players').setDescription('Get a list of online players'),
new SlashCommandBuilder().setName('my-website-url').setDescription('Get the server website URL'),
2024-10-02 02:31:03 -04:00
new SlashCommandBuilder().setName('bluemap-url').setDescription('Get the server map URL'),
2024-10-02 02:27:21 -04:00
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)),
2024-10-01 23:31:27 -04:00
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)),
2024-10-02 02:27:21 -04:00
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)),
2024-10-01 23:31:27 -04:00
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)),
2024-10-02 02:27:21 -04:00
new SlashCommandBuilder().setName('mod-list').setDescription('Get a list of installed mods'),
2024-10-01 23:31:27 -04:00
];
// 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) {
2024-10-01 23:38:26 -04:00
// case 'server-time':
// const timeData = await handleApiCall(() => MyMC.getTime(), userId, interaction);
// handleResponse(timeData, interaction);
// break;
2024-10-01 23:31:27 -04:00
2024-10-02 02:27:21 -04:00
case 'stats':
2024-10-01 23:31:27 -04:00
const stats = await handleApiCall(() => MyMC.getStats(), userId, interaction);
handleResponse(stats, interaction);
break;
2024-10-02 02:27:21 -04:00
case 'logs':
2024-10-01 23:31:27 -04:00
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;
2024-10-02 02:27:21 -04:00
case 'my-mc-link':
2024-10-02 00:06:33 -04:00
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) {
2024-10-02 02:27:21 -04:00
console.error('Error during my-mc-link command:', error);
2024-10-02 00:06:33 -04:00
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;
2024-10-01 23:31:27 -04:00
2024-10-02 02:27:21 -04:00
case 'my-mc-link':
2024-10-02 00:06:33 -04:00
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) {
2024-10-02 02:27:21 -04:00
console.error('Error during my-mc-link command:', error);
2024-10-02 00:06:33 -04:00
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;
2024-10-01 23:31:27 -04:00
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;
2024-10-02 02:27:21 -04:00
case 'list-players':
2024-10-01 23:31:27 -04:00
const players = await handleApiCall(() => MyMC.getOnlinePlayers(), userId, interaction);
handleResponse(players, interaction);
break;
2024-10-02 02:27:21 -04:00
case 'my-website-url':
2024-10-01 23:31:27 -04:00
const websiteURL = await handleApiCall(() => MyMC.getWebsiteURL(), userId, interaction);
handleResponse(websiteURL, interaction);
break;
2024-10-02 02:31:03 -04:00
case 'bluemap-url':
2024-10-01 23:31:27 -04:00
const mapURL = await handleApiCall(() => MyMC.getMapURL(), userId, interaction);
handleResponse(mapURL, interaction);
break;
2024-10-02 02:27:21 -04:00
case 'ban':
2024-10-01 23:31:27 -04:00
const banUsername = interaction.options.getString('username');
const banResult = await handleApiCall(() => MyMC.postBan(banUsername), userId, interaction);
handleResponse(banResult, interaction, true);
break;
2024-10-02 02:27:21 -04:00
case 'unban':
2024-10-01 23:31:27 -04:00
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;
2024-10-02 02:27:21 -04:00
case 'mc-cmd':
2024-10-01 23:31:27 -04:00
const command = interaction.options.getString('command');
const consoleResult = await handleApiCall(() => MyMC.postConsole(command), userId, interaction);
handleResponse(consoleResult, interaction, true);
break;
2024-10-02 02:27:21 -04:00
case 'give':
2024-10-01 23:31:27 -04:00
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;
2024-10-02 02:27:21 -04:00
case 'mod-list':
2024-10-01 23:31:27 -04:00
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);