first commit
This commit is contained in:
commit
35e4c48991
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
tokens.json
|
||||
config.json
|
||||
package-lock.json
|
||||
node_modules
|
380
README.md
Normal file
380
README.md
Normal file
@ -0,0 +1,380 @@
|
||||
# Discord Container Manager Bot
|
||||
|
||||
A powerful Discord bot built with [discord.js](https://discord.js.org/) that allows users to manage and interact with containerized services directly from Discord. The bot integrates with a MySQL database for user management and communicates with an external API to perform various container operations such as starting, stopping, restarting containers, fetching stats, and executing commands.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Config Files](#config-files)
|
||||
- [Database Setup](#database-setup)
|
||||
- [Usage](#usage)
|
||||
- [Available Commands](#available-commands)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
|
||||
## Features
|
||||
|
||||
- **Slash Commands:** Interact with containers using intuitive slash commands.
|
||||
- **Database Integration:** Securely manage user data with MySQL.
|
||||
- **API Communication:** Fetch and manage tokens automatically, ensuring secure API interactions.
|
||||
- **Dynamic Command Registration:** Automatically registers and updates commands with Discord.
|
||||
- **Embed Messages:** Provides rich and informative responses using Discord embeds.
|
||||
- **Command Execution:** Execute shell commands within containers directly from Discord.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have met the following requirements:
|
||||
|
||||
- **Node.js:** Version 16.6.0 or higher. [Download Node.js](https://nodejs.org/)
|
||||
- **MySQL Database:** A running MySQL server to store user data.
|
||||
- **Discord Bot:** A Discord application with a bot token. [Create a Discord Bot](https://discord.com/developers/applications)
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the Repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/discord-container-manager.git
|
||||
cd discord-container-manager
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
|
||||
Ensure you have [Node.js](https://nodejs.org/) installed. Then, install the required npm packages:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
The project relies on the following main dependencies:
|
||||
|
||||
- `discord.js`: Interact with the Discord API.
|
||||
- `mysql2`: Connect to the MySQL database.
|
||||
- `jsonfile`: Read and write JSON files.
|
||||
- `unirest`: Make HTTP requests.
|
||||
- `fs`: File system operations (built-in Node.js module).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config Files
|
||||
|
||||
The bot requires two main configuration files: `config.json` and `tokens.json`.
|
||||
|
||||
1. **config.json**
|
||||
|
||||
This file holds essential configuration details such as Discord tokens, API endpoints, and database credentials.
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"clientId": "YOUR_DISCORD_CLIENT_ID",
|
||||
"SQLHOST": "localhost",
|
||||
"SQLUSER": "your_mysql_user",
|
||||
"SQLDATABASE": "your_database",
|
||||
"SQLPASSWORD": "your_mysql_password",
|
||||
"endpoint": "https://api.yourservice.com",
|
||||
"password": "YOUR_API_PASSWORD",
|
||||
"apiBaseURL": "https://api.yourservice.com"
|
||||
}
|
||||
```
|
||||
|
||||
- **token:** Your Discord bot token.
|
||||
- **clientId:** Your Discord application's client ID.
|
||||
- **SQLHOST:** Hostname for your MySQL server.
|
||||
- **SQLUSER:** MySQL username.
|
||||
- **SQLDATABASE:** Name of the MySQL database.
|
||||
- **SQLPASSWORD:** MySQL user password.
|
||||
- **endpoint:** API endpoint for token fetching.
|
||||
- **password:** Password used for API authentication.
|
||||
- **apiBaseURL:** Base URL for the API interactions.
|
||||
|
||||
2. **tokens.json**
|
||||
|
||||
This file is used to store and manage user-specific API tokens. It's automatically generated and managed by the bot. **Ensure this file is kept secure and is excluded from version control.**
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
> **Note:** It's recommended to add `tokens.json` and `config.json` to your `.gitignore` to prevent sensitive information from being pushed to version control.
|
||||
|
||||
### Database Setup
|
||||
|
||||
The bot connects to a MySQL database to manage user data. Ensure your database has a `users` table with the following structure:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
uid VARCHAR(255) NOT NULL,
|
||||
discord_id VARCHAR(255) UNIQUE NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
- **id:** Primary key.
|
||||
- **uid:** Unique identifier for the user in the external system.
|
||||
- **discord_id:** Discord user ID.
|
||||
|
||||
> **Note:** Adjust the table schema as needed based on your requirements.
|
||||
|
||||
## Usage
|
||||
|
||||
After completing the installation and configuration steps, you can start the bot using:
|
||||
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
||||
Upon successful startup, the bot will register its slash commands with Discord and begin listening for interactions.
|
||||
|
||||
### Available Commands
|
||||
|
||||
The bot offers a variety of slash commands to manage containers and interact with the underlying API.
|
||||
|
||||
#### `/hello`
|
||||
|
||||
**Description:** Say hello via API.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/hello
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message with a greeting from the API.
|
||||
|
||||
#### `/name`
|
||||
|
||||
**Description:** Get the API username.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/name
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message displaying the API username.
|
||||
|
||||
#### `/start`
|
||||
|
||||
**Description:** Start the container.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/start
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message confirming the container has started.
|
||||
|
||||
#### `/stop`
|
||||
|
||||
**Description:** Stop the container.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/stop
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message confirming the container has stopped.
|
||||
|
||||
#### `/restart`
|
||||
|
||||
**Description:** Restart the container.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/restart
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message confirming the container has restarted.
|
||||
|
||||
#### `/info`
|
||||
|
||||
**Description:** Get container information.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/info
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message detailing various information about the container, including name, IP address, memory usage, CPU usage, status, and more.
|
||||
|
||||
#### `/stats`
|
||||
|
||||
**Description:** Get container stats.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/stats
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message displaying memory and CPU usage statistics of the container.
|
||||
|
||||
#### `/time`
|
||||
|
||||
**Description:** Get container expire time.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/time
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An embed message showing the expiration date of the container.
|
||||
|
||||
#### `/root-password`
|
||||
|
||||
**Description:** Change the root password.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/root-password
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An ephemeral embed message revealing the new root password.
|
||||
|
||||
#### `/new-api-key`
|
||||
|
||||
**Description:** Generate a new API key.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/new-api-key
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An ephemeral embed message providing a new API key.
|
||||
|
||||
#### `/key-expire-time`
|
||||
|
||||
**Description:** Check the API key expiration time.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/key-expire-time
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
An ephemeral embed message showing the expiration date of the API key.
|
||||
|
||||
#### `/x`
|
||||
|
||||
**Description:** Execute a command in the container.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/x command: <your_command>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- **command** (String, Required): The command to execute inside the container.
|
||||
|
||||
**Response:**
|
||||
|
||||
A message containing the standard output and error from the executed command, formatted in markdown code blocks.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Change directory:
|
||||
|
||||
```
|
||||
/x command: cd /var/www
|
||||
```
|
||||
|
||||
- List files:
|
||||
|
||||
```
|
||||
/x command: ls -la
|
||||
```
|
||||
|
||||
#### `/notify`
|
||||
|
||||
**Description:** Send a notification to Discord.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/notify message: <your_message>
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- **message** (String, Required): The message to send as a notification.
|
||||
|
||||
**Response:**
|
||||
|
||||
An ephemeral embed message confirming the notification has been sent.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Follow these steps to contribute:
|
||||
|
||||
1. **Fork the Repository**
|
||||
|
||||
Click the [Fork](https://github.com/yourusername/discord-container-manager/fork) button on the repository page.
|
||||
|
||||
2. **Create a New Branch**
|
||||
|
||||
```bash
|
||||
git checkout -b feature/YourFeature
|
||||
```
|
||||
|
||||
3. **Make Your Changes**
|
||||
|
||||
Implement your feature or fix the bug.
|
||||
|
||||
4. **Commit Your Changes**
|
||||
|
||||
```bash
|
||||
git commit -m "Add your message here"
|
||||
```
|
||||
|
||||
5. **Push to the Branch**
|
||||
|
||||
```bash
|
||||
git push origin feature/YourFeature
|
||||
```
|
||||
|
||||
6. **Create a Pull Request**
|
||||
|
||||
Navigate to the original repository and create a pull request from your forked branch.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [discord.js](https://discord.js.org/) - Powerful library for interacting with the Discord API.
|
||||
- [Unirest](http://unirest.io/) - Lightweight HTTP client.
|
||||
- [mysql2](https://github.com/sidorares/node-mysql2) - MySQL client for Node.js.
|
||||
- [jsonfile](https://github.com/jprichardson/node-jsonfile) - Easily read/write JSON files.
|
18
package.json
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "installable-bot",
|
||||
"version": "1.0.0",
|
||||
"main": "user-bot.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.16.3",
|
||||
"jsonfile": "^6.1.0",
|
||||
"mysql2": "^3.11.3",
|
||||
"unirest": "^0.6.0"
|
||||
}
|
||||
}
|
426
user-bot.js
Normal file
426
user-bot.js
Normal file
@ -0,0 +1,426 @@
|
||||
import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes, EmbedBuilder } from 'discord.js';
|
||||
import jsonfile from 'jsonfile';
|
||||
import unirest from 'unirest';
|
||||
import { readFileSync } from 'fs';
|
||||
import mysql from 'mysql2';
|
||||
const userWorkingDirectories = new Map();
|
||||
|
||||
let sshSurfID; // Variable to store the user ID from the database
|
||||
|
||||
// Paths to config and tokens files
|
||||
const tokensFile = './tokens.json';
|
||||
const config = JSON.parse(readFileSync('./config.json', 'utf8'));
|
||||
|
||||
// MySQL connection
|
||||
const connection = mysql.createConnection({
|
||||
host: config.SQLHOST,
|
||||
user: config.SQLUSER,
|
||||
database: config.SQLDATABASE,
|
||||
password: config.SQLPASSWORD
|
||||
});
|
||||
|
||||
// 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(sshSurfID, interaction) {
|
||||
return unirest
|
||||
.post(config.endpoint.toString())
|
||||
.headers({ 'Accept': 'application/json', 'Content-Type': 'application/json' })
|
||||
.send({ "username": `${sshSurfID}`, "password": config.password.toString() })
|
||||
.then((tokenInfo) => {
|
||||
const tokens = loadTokens();
|
||||
tokens[sshSurfID] = tokenInfo.body.token; // Save the new token for sshSurfID
|
||||
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(sshSurfID, interaction) {
|
||||
const tokens = loadTokens();
|
||||
if (!tokens[sshSurfID]) {
|
||||
return await fetchAndSaveToken(sshSurfID, interaction);
|
||||
}
|
||||
return tokens[sshSurfID];
|
||||
}
|
||||
|
||||
// Handle API request
|
||||
async function makeApiRequest(endpoint, token, interaction, method = 'get', body = null) {
|
||||
const request = unirest[method](config.apiBaseURL + endpoint)
|
||||
.headers({
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'x-ssh-auth': token
|
||||
});
|
||||
|
||||
if (body) {
|
||||
request.send(body);
|
||||
}
|
||||
|
||||
return request.then(response => {
|
||||
if (response.error) {
|
||||
console.error('API Error:', response.error);
|
||||
sendSexyEmbed("Error", "An error occurred while communicating with the API.", interaction);
|
||||
throw response.error;
|
||||
}
|
||||
return response.body;
|
||||
});
|
||||
}
|
||||
|
||||
// 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.editReply({
|
||||
embeds: [embed],
|
||||
ephemeral: ephemeral // Set ephemeral flag based on the condition
|
||||
});
|
||||
}
|
||||
|
||||
// Send sexy embed with fields
|
||||
function sendSexyEmbedWithFields(title, fields, interaction, ephemeral = false) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor("#3498DB")
|
||||
.setTitle(title)
|
||||
.addFields(fields)
|
||||
.setTimestamp()
|
||||
.setFooter({
|
||||
text: `Requested by ${interaction.user.username}`,
|
||||
iconURL: `${interaction.user.displayAvatarURL()}`
|
||||
});
|
||||
interaction.editReply({
|
||||
embeds: [embed],
|
||||
ephemeral: ephemeral // Set ephemeral flag based on the condition
|
||||
});
|
||||
}
|
||||
|
||||
// Slash command definitions
|
||||
const commands = [
|
||||
new SlashCommandBuilder().setName('hello').setDescription('Say hello via API'),
|
||||
new SlashCommandBuilder().setName('name').setDescription('Get the API username'),
|
||||
new SlashCommandBuilder().setName('start').setDescription('Start the container'),
|
||||
new SlashCommandBuilder().setName('stop').setDescription('Stop the container'),
|
||||
new SlashCommandBuilder().setName('restart').setDescription('Restart the container'),
|
||||
new SlashCommandBuilder().setName('info').setDescription('Get container information'),
|
||||
new SlashCommandBuilder().setName('stats').setDescription('Get container stats'),
|
||||
new SlashCommandBuilder().setName('time').setDescription('Get container expire time'),
|
||||
new SlashCommandBuilder().setName('root-password').setDescription('Change the root password'),
|
||||
new SlashCommandBuilder().setName('new-api-key').setDescription('Generate a new API key'),
|
||||
new SlashCommandBuilder().setName('key-expire-time').setDescription('Check the key expire time'),
|
||||
new SlashCommandBuilder().setName('x').setDescription('Execute a command in the container')
|
||||
.addStringOption(option => option.setName('command').setDescription('Command to execute').setRequired(true)),
|
||||
new SlashCommandBuilder().setName('notify').setDescription('Send a notification to Discord')
|
||||
.addStringOption(option => option.setName('message').setDescription('Message to send').setRequired(true)),
|
||||
];
|
||||
|
||||
// Register commands with Discord
|
||||
const rest = new REST({ version: '10' }).setToken(config.token);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
|
||||
// Add extra fields to each command
|
||||
const commandsWithExtras = commands.map((command) => {
|
||||
const jsonCommand = 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
|
||||
};
|
||||
|
||||
// Add extras to the command's JSON object
|
||||
Object.keys(extras).forEach(key => jsonCommand[key] = extras[key]);
|
||||
|
||||
return jsonCommand;
|
||||
});
|
||||
|
||||
// Register commands with Discord, making sure all commands are sent in an array
|
||||
const data = await rest.put(
|
||||
Routes.applicationCommands(config.clientId),
|
||||
{ body: commandsWithExtras } // Send all commands in one array
|
||||
);
|
||||
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error) {
|
||||
console.error('Error reloading commands:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Handle bot interactions
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isCommand()) return;
|
||||
|
||||
// Defer the reply to allow time for the command to run
|
||||
await interaction.deferReply();
|
||||
|
||||
// First, we fetch the sshSurfID from the database using interaction.user.id
|
||||
let sshSurfID = await new Promise((resolve, reject) => {
|
||||
connection.query(
|
||||
"SELECT uid FROM users WHERE discord_id = ?",
|
||||
[interaction.user.id],
|
||||
(err, results) => {
|
||||
if (err) {
|
||||
console.error('Error querying database:', err);
|
||||
reject(err);
|
||||
} else if (results.length === 0) {
|
||||
console.log("User does not exist");
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(results[0].uid);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!sshSurfID) {
|
||||
return sendSexyEmbed("Error", "User not found in the database.", interaction);
|
||||
}
|
||||
|
||||
// Once sshSurfID is set, we proceed with token fetching and API requests
|
||||
const apiToken = await getToken(sshSurfID, interaction);
|
||||
|
||||
try {
|
||||
switch (interaction.commandName) {
|
||||
case 'hello':
|
||||
const helloResponse = await makeApiRequest('/hello', apiToken, interaction);
|
||||
sendSexyEmbed("Hello", `Message: ${helloResponse.message}`, interaction);
|
||||
break;
|
||||
|
||||
case 'name':
|
||||
const nameResponse = await makeApiRequest('/name', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Username', [
|
||||
{ name: 'Username', value: nameResponse.message }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'start':
|
||||
const startResponse = await makeApiRequest('/start', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Start Server', [
|
||||
{ name: 'Status', value: 'Success' },
|
||||
{ name: 'Message', value: startResponse.message }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
const stopResponse = await makeApiRequest('/stop', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Stop Server', [
|
||||
{ name: 'Status', value: 'Success' },
|
||||
{ name: 'Message', value: stopResponse.message }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'restart':
|
||||
const restartResponse = await makeApiRequest('/restart', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Restart Server', [
|
||||
{ name: 'Status', value: 'Success' },
|
||||
{ name: 'Message', value: restartResponse.message }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'info':
|
||||
const infoResponse = await makeApiRequest('/info', apiToken, interaction);
|
||||
|
||||
// Extract and fallback data fields from the response
|
||||
const containerName = infoResponse.data?.name || 'N/A';
|
||||
const ipAddress = infoResponse.data?.IPAddress || 'N/A';
|
||||
const macAddress = infoResponse.data?.MacAddress || 'N/A';
|
||||
const memory = infoResponse.data?.memory || 'N/A';
|
||||
const cpus = infoResponse.data?.cpus || 'N/A';
|
||||
const restartPolicy = infoResponse.data?.restartPolicy?.Name || 'N/A';
|
||||
const restarts = infoResponse.data?.restarts !== undefined ? infoResponse.data.restarts : 'N/A';
|
||||
const status = infoResponse.data?.state?.Status || 'Unknown';
|
||||
const pid = infoResponse.data?.state?.Pid || 'N/A';
|
||||
const startedAt = infoResponse.data?.state?.StartedAt || 'N/A';
|
||||
const image = infoResponse.data?.image || 'N/A';
|
||||
const createdAt = infoResponse.data?.created || 'N/A';
|
||||
|
||||
// Format and send the embed
|
||||
sendSexyEmbedWithFields('Container Info', [
|
||||
{ name: 'Name', value: containerName },
|
||||
{ name: 'IP Address', value: ipAddress },
|
||||
{ name: 'MAC Address', value: macAddress },
|
||||
{ name: 'Memory', value: memory },
|
||||
{ name: 'CPUs', value: cpus },
|
||||
{ name: 'Restart Policy', value: restartPolicy },
|
||||
{ name: 'Restarts', value: `${restarts}` },
|
||||
{ name: 'Status', value: status },
|
||||
{ name: 'PID', value: `${pid}` },
|
||||
{ name: 'Started At', value: startedAt },
|
||||
{ name: 'Created At', value: createdAt }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
const statsResponse = await makeApiRequest('/stats', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Container Stats', [
|
||||
{ name: 'Memory Usage', value: `${statsResponse.data.memory.raw} (${statsResponse.data.memory.percent})` },
|
||||
{ name: 'CPU Usage', value: statsResponse.data.cpu }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'time':
|
||||
const timeResponse = await makeApiRequest('/time', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Container Expire Time', [
|
||||
{ name: 'Expire Date', value: timeResponse.expireDate }
|
||||
], interaction);
|
||||
break;
|
||||
|
||||
case 'root-password':
|
||||
const rootPassResponse = await makeApiRequest('/rootpass', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('Root Password', [
|
||||
{ name: 'New Root Password', value: rootPassResponse.newRootPass }
|
||||
], interaction, true);
|
||||
break;
|
||||
|
||||
case 'new-api-key':
|
||||
const newKeyResponse = await makeApiRequest('/new-key', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('New API Key', [
|
||||
{ name: 'New API Key', value: newKeyResponse.newAPIKey }
|
||||
], interaction, true);
|
||||
break;
|
||||
|
||||
case 'key-expire-time':
|
||||
const keyTimeResponse = await makeApiRequest('/key-time', apiToken, interaction);
|
||||
sendSexyEmbedWithFields('API Key Expire Time', [
|
||||
{ name: 'Expire Date', value: keyTimeResponse.expireDate }
|
||||
], interaction, true);
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 'x': {
|
||||
|
||||
const command = interaction.options.getString('command');
|
||||
|
||||
// Get the user's current working directory or default to root (/)
|
||||
let userPWD = userWorkingDirectories.get(sshSurfID) || '/';
|
||||
|
||||
// Handle 'cd' command logic
|
||||
if (command.startsWith('cd')) {
|
||||
let argscmd = command.replace('cd ', '').trim();
|
||||
|
||||
// Handle 'cd ..' for going up one directory
|
||||
if (argscmd === '..') {
|
||||
if (userPWD !== '/') {
|
||||
// Remove the last part of the current path
|
||||
const newPWD = userPWD.split('/').slice(0, -1).join('/') || '/';
|
||||
userPWD = newPWD;
|
||||
userWorkingDirectories.set(sshSurfID, newPWD);
|
||||
await interaction.editReply(`Directory changed to: ${newPWD}`);
|
||||
} else {
|
||||
await interaction.editReply(`Already at the root directory: ${userPWD}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle '~' for home directory
|
||||
if (argscmd === '~') {
|
||||
userPWD = '/root';
|
||||
userWorkingDirectories.set(sshSurfID, userPWD);
|
||||
await interaction.editReply(`Directory changed to: ${userPWD}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle absolute and relative paths
|
||||
let newPWD;
|
||||
if (argscmd.startsWith('/')) {
|
||||
// Absolute path
|
||||
newPWD = argscmd;
|
||||
} else {
|
||||
// Relative path
|
||||
newPWD = `${userPWD}/${argscmd}`;
|
||||
}
|
||||
|
||||
// Normalize the path (remove extra slashes)
|
||||
newPWD = newPWD.replace(/([^:]\/)\/+/g, "$1");
|
||||
|
||||
// Check if the user is trying to go back multiple directories (e.g., 'cd ../../')
|
||||
if (argscmd.includes('../')) {
|
||||
const numDirsBack = argscmd.split('../').length - 1;
|
||||
newPWD = RemoveLastDirectoryPartOf(userPWD, numDirsBack);
|
||||
}
|
||||
|
||||
// Update the working directory
|
||||
userWorkingDirectories.set(sshSurfID, newPWD);
|
||||
await interaction.editReply(`Directory changed to: ${newPWD}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the command is not 'cd', run the command in the current working directory (or default to '/')
|
||||
const execResponse = await makeApiRequest('/exec', apiToken, interaction, 'post', {
|
||||
cmd: command,
|
||||
pwd: userPWD // Use the current directory or default to '/'
|
||||
});
|
||||
|
||||
// Format the command output in a markdown code block
|
||||
let replyMessage = `\`\`\`\n${execResponse.stdout || 'No output'}\n\`\`\``;
|
||||
|
||||
// If there is an error, append the error message in another markdown code block
|
||||
if (execResponse.stderr && execResponse.stderr.trim()) {
|
||||
replyMessage += `\n**Error:**\n\`\`\`\n${execResponse.stderr}\n\`\`\``;
|
||||
}
|
||||
|
||||
// Reply with the formatted message
|
||||
await interaction.editReply(replyMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
// Helper function to remove directories when using '../'
|
||||
function RemoveLastDirectoryPartOf(the_url, num) {
|
||||
var the_arr = the_url.split('/');
|
||||
the_arr.splice(-num, num);
|
||||
return the_arr.join('/') || '/';
|
||||
}
|
||||
|
||||
|
||||
case 'notify':
|
||||
const message = interaction.options.getString('message');
|
||||
const notifyResponse = await makeApiRequest('/notify', apiToken, interaction, 'post', {
|
||||
message: message
|
||||
});
|
||||
sendSexyEmbedWithFields('Notification', [
|
||||
{ name: 'Status', value: 'Success' },
|
||||
{ name: 'Message', value: notifyResponse.message }
|
||||
], interaction, true);
|
||||
break;
|
||||
|
||||
default:
|
||||
interaction.reply('Command not recognized.');
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Command error:', error);
|
||||
sendSexyEmbed('Error', 'An error occurred while processing your request.', interaction);
|
||||
}
|
||||
});
|
||||
|
||||
// Log in to Discord
|
||||
client.login(config.token);
|
Loading…
Reference in New Issue
Block a user