427 lines
15 KiB
JavaScript
427 lines
15 KiB
JavaScript
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);
|