first commit

This commit is contained in:
MCHost 2024-10-01 23:31:27 -04:00
commit b06b8f1a01
4 changed files with 531 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
config.json
package-lock.json
tokens.json
node_modules

157
README.md Normal file
View File

@ -0,0 +1,157 @@
Here's a detailed `README.md` file for the provided code, which is a Discord bot for managing Minecraft servers using the MyMCLib API. This README includes installation instructions, configuration steps, usage, and a breakdown of the commands.
### README.md
---
# My-MC Link Discord Bot (BETA)
Welcome to the **My-MC Link Discord Bot**, a Discord bot designed to manage your Minecraft servers directly through Discord commands! This bot uses the MyMCLib API to interact with your server, allowing you to perform essential tasks like starting/stopping servers, managing players, installing mods, and much more.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Commands](#commands)
- [Token Handling](#token-handling)
- [Usage](#usage)
- [Contributing](#contributing)
## Features
- Start, stop, and restart your Minecraft server with simple commands.
- Check real-time server statistics like memory usage, CPU, and more.
- Manage player bans/unbans, send messages to players, and view online players.
- Install, uninstall, and list mods directly from Discord.
- View real-time server logs and connection details (SFTP, P2P, etc.).
- Works as a **user-installable app**!
## Installation
### Prerequisites
- **Node.js**: Ensure you have Node.js (v16 or later) installed on your machine.
- **npm**: Comes bundled with Node.js for installing dependencies.
- **Discord Bot Token**: You will need to create a Discord bot and get its token. [Learn how](https://discordjs.guide/preparations/setting-up-a-bot-application.html#creating-your-bot).
### Steps
1. **Clone the Repository**
```bash
git clone https://git.ssh.surf/hypermc/hypermc-api-user-install-bot
cd hypermc-api-user-install-bot
```
2. **Install Dependencies**
Install all necessary dependencies using `npm`:
```bash
npm install
```
3. **Configuration**
You need to create a `config.json` file for the bot's configuration. Here's an example:
```json
{
"token": "YOUR_DISCORD_BOT_TOKEN",
"clientId": "YOUR_DISCORD_CLIENT_ID",
"endpoint": "YOUR_AUTHENTICATION_ENDPOINT",
"password": "YOUR_AUTHENTICATION_PASSWORD"
}
```
4. **Run the Bot**
Once everything is configured, start the bot:
```bash
node bot.js
```
## Configuration
The `config.json` file must contain the following:
- **token**: The token for your Discord bot.
- **clientId**: The client ID of your bot from the Discord developer portal.
- **endpoint**: The authentication endpoint for obtaining user API tokens.
- **password**: The password required by the authentication service.
Here is an example `config.json` file:
```json
{
"token": "YOUR_DISCORD_BOT_TOKEN",
"clientId": "YOUR_DISCORD_CLIENT_ID",
"endpoint": "https://your-auth-endpoint.com/auth",
"password": "your-auth-password"
}
```
## Commands
The bot provides the following commands to manage your Minecraft server:
- `/server-time` Get the current server time.
- `/server-stats` Get server statistics, including CPU and memory usage.
- `/server-log` Retrieve the real-time server log URL.
- `/start-server` Start the Minecraft server.
- `/stop-server` Stop the Minecraft server.
- `/restart-server` Restart the Minecraft server.
- `/create-link` Create a custom server link.
- `/create-sftp-link` Create an SFTP link.
- `/get-connection-hash` Retrieve the server's P2P connection hash.
- `/get-connection-hash-sftp` Retrieve the server's SFTP connection hash.
- `/get-players` Get a list of players currently online on the server.
- `/get-website-url` Get the server's website URL.
- `/get-map-url` Get the server's map URL.
- `/ban-player` Ban a player by username.
- `/unban-player` Unban a player by username.
- `/send-message` Send a message to all players on the server.
- `/send-private-message` Send a private message to a specific player.
- `/execute-console-command` Execute a command in the server console.
- `/give-item` Give an item to a player.
- `/install-mod` Install a mod on the server by its ID.
- `/uninstall-mod` Uninstall a mod from the server by its ID.
- `/get-installed-mods` List all installed mods on the server.
## Token Handling
This bot automatically handles user API tokens. When a user runs any command, the bot checks if a valid token exists for that user. If not, it fetches and stores a new token via the configured authentication endpoint.
Tokens are stored in a `tokens.json` file on the server.
- **fetchAndSaveToken**: Automatically requests and stores a new API token if needed.
- **getToken**: Retrieves a stored token or requests a new one if none exists or if the token is invalid.
### Token Persistence
The tokens are stored locally in a `tokens.json` file using the `jsonfile` package:
```json
{
"userId": "user-token"
}
```
The bot will load tokens from this file when needed and automatically refresh them if expired or invalid.
## Usage
Once the bot is up and running, invite it to your Discord server. Users can run commands like `/server-time` or `/start-server` to interact with the Minecraft server.
Commands can be run either in public channels or privately, depending on how you've set up the bot. Some responses can also be made ephemeral (visible only to the user who executed the command).
For example:
- Running `/server-stats` will display current server stats like CPU and memory usage.
- Running `/install-mod modid` will install a specified mod on the server.
### Example Command Usage
```bash
/server-stats
# Displays server statistics
/ban-player username:exampleUser
# Bans 'exampleUser' from the Minecraft server
/give-item username:exampleUser item:diamond amount:5
# Gives 5 diamonds to 'exampleUser'
```
## Contributing
Feel free to open an issue or submit a pull request if you'd like to contribute to this project. Any improvements or additional features are welcome!

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"dependencies": {
"discord.js": "^14.16.3",
"jsonfile": "^6.1.0",
"mymc-lib": "^1.1.0"
},
"name": "public-bot",
"version": "1.0.0",
"type": "module",
"main": "public_bot.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

352
public_bot.js Normal file
View File

@ -0,0 +1,352 @@
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';
// 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)
.headers({ 'Accept': 'application/json', 'Content-Type': 'application/json' })
.send({ "username": `mc_${userId}`, "password": config.password})
.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 : '';
// 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('server-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':
const customLinkResult = await handleApiCall(() => MyMC.createMyLink(), userId, interaction);
handleResponse(customLinkResult, interaction);
break;
case 'create-sftp-link':
const sftpLinkResult = await handleApiCall(() => MyMC.createLinkSFTP(), userId, interaction);
handleResponse(sftpLinkResult, 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);