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('server-stats').setDescription('Get the server statistics'), new SlashCommandBuilder().setName('server-log').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('create-link').setDescription('Create a custom server link'), new SlashCommandBuilder().setName('create-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('get-players').setDescription('Get a list of online players'), new SlashCommandBuilder().setName('get-website-url').setDescription('Get the server website URL'), new SlashCommandBuilder().setName('get-map-url').setDescription('Get the server map URL'), new SlashCommandBuilder().setName('ban-player').setDescription('Ban a player by username').addStringOption(option => option.setName('username').setDescription('Player username').setRequired(true)), new SlashCommandBuilder().setName('unban-player').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('execute-console-command').setDescription('Execute a command in the server console').addStringOption(option => option.setName('command').setDescription('Command to execute').setRequired(true)), new SlashCommandBuilder().setName('give-item').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('get-installed-mods').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 'server-stats': const stats = await handleApiCall(() => MyMC.getStats(), userId, interaction); handleResponse(stats, interaction); break; case 'server-log': 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 'create-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 create-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 'create-sftp-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 create-sftp-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 'get-players': const players = await handleApiCall(() => MyMC.getOnlinePlayers(), userId, interaction); handleResponse(players, interaction); break; case 'get-website-url': const websiteURL = await handleApiCall(() => MyMC.getWebsiteURL(), userId, interaction); handleResponse(websiteURL, interaction); break; case 'get-map-url': const mapURL = await handleApiCall(() => MyMC.getMapURL(), userId, interaction); handleResponse(mapURL, interaction); break; case 'ban-player': const banUsername = interaction.options.getString('username'); const banResult = await handleApiCall(() => MyMC.postBan(banUsername), userId, interaction); handleResponse(banResult, interaction, true); break; case 'unban-player': 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 'execute-console-command': const command = interaction.options.getString('command'); const consoleResult = await handleApiCall(() => MyMC.postConsole(command), userId, interaction); handleResponse(consoleResult, interaction, true); break; case 'give-item': 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 'get-installed-mods': 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);