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(); import fs from 'fs'; import cmd from 'cmd-promise'; let sshSurfID; // Variable to store the user ID from the database // Paths to config and tokens files const tokensFile = './tokens.json'; const privacyFile = './user_privacy.json'; // Privacy file path 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 }); } // Load privacy settings from JSON file function loadUserPrivacySettings() { try { return jsonfile.readFileSync(privacyFile); } catch (error) { console.error('Error reading privacy settings file:', error); return {}; } } // Save user privacy settings to JSON file function saveUserPrivacySettings() { jsonfile.writeFileSync(privacyFile, userPrivacySettings, { spaces: 2 }); } // Load user privacy settings on startup let userPrivacySettings = loadUserPrivacySettings(); // 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 }); } // Toggle privacy for the user async function togglePrivacy(interaction) { const userId = interaction.user.id; const currentSetting = userPrivacySettings[userId] || false; userPrivacySettings[userId] = !currentSetting; // Toggle the setting saveUserPrivacySettings(); const message = userPrivacySettings[userId] ? 'Your responses are now set to ephemeral (visible only to you).' : 'Your responses are now standard (visible to everyone).'; await interaction.editReply({ content: message, ephemeral: true }); } // Check if the user's responses should be ephemeral function isEphemeral(userId) { return userPrivacySettings[userId] || false; // Default to false (standard response) if not set } // 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)), new SlashCommandBuilder().setName('privacy').setDescription('Toggle ephemeral responses') // New command to toggle privacy ]; // 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; // Check if the command is /privacy, always make it ephemeral const isPrivacyCommand = interaction.commandName === 'privacy'; // Defer the reply based on whether it's the privacy command or not await interaction.deferReply({ ephemeral: isPrivacyCommand || isEphemeral(interaction.user.id) }); // 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.\nJoin our server to gain access: https://join.discord-linux.com", interaction); } // Handle privacy toggle command if (interaction.commandName === 'privacy') { await togglePrivacy(interaction); return; } // 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, isEphemeral(interaction.user.id)); break; case 'name': const nameResponse = await makeApiRequest('/name', apiToken, interaction); sendSexyEmbedWithFields('Username', [ { name: 'Username', value: nameResponse.message } ], interaction, isEphemeral(interaction.user.id)); break; case 'start': const startResponse = await makeApiRequest('/start', apiToken, interaction); sendSexyEmbedWithFields('Start Server', [ { name: 'Status', value: 'Success' }, { name: 'Message', value: startResponse.message } ], interaction, isEphemeral(interaction.user.id)); break; case 'stop': const stopResponse = await makeApiRequest('/stop', apiToken, interaction); sendSexyEmbedWithFields('Stop Server', [ { name: 'Status', value: 'Success' }, { name: 'Message', value: stopResponse.message } ], interaction, isEphemeral(interaction.user.id)); break; case 'restart': const restartResponse = await makeApiRequest('/restart', apiToken, interaction); sendSexyEmbedWithFields('Restart Server', [ { name: 'Status', value: 'Success' }, { name: 'Message', value: restartResponse.message } ], interaction, isEphemeral(interaction.user.id)); break; case 'info': const infoResponse = await makeApiRequest('/info', apiToken, interaction); 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'; 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, isEphemeral(interaction.user.id)); 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, isEphemeral(interaction.user.id)); break; case 'time': const timeResponse = await makeApiRequest('/time', apiToken, interaction); sendSexyEmbedWithFields('Container Expire Time', [ { name: 'Expire Date', value: timeResponse.expireDate } ], interaction, isEphemeral(interaction.user.id)); break; case 'root-password': const rootPassResponse = await makeApiRequest('/rootpass', apiToken, interaction); sendSexyEmbedWithFields('Root Password', [ { name: 'New Root Password', value: rootPassResponse.newRootPass } ], interaction, isEphemeral(interaction.user.id)); 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, isEphemeral(interaction.user.id)); 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, isEphemeral(interaction.user.id)); break; case 'x': const command = interaction.options.getString('command'); let userPWD = userWorkingDirectories.get(sshSurfID) || '/'; if (command.startsWith('cd')) { let argscmd = command.replace('cd ', '').trim(); if (argscmd === '..') { if (userPWD !== '/') { 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; } if (argscmd === '~') { userPWD = '/root'; userWorkingDirectories.set(sshSurfID, userPWD); await interaction.editReply(`Directory changed to: ${userPWD}`); return; } let newPWD; if (argscmd.startsWith('/')) { newPWD = argscmd; } else { newPWD = `${userPWD}/${argscmd}`; } newPWD = newPWD.replace(/([^:]\/)\/+/g, "$1"); if (argscmd.includes('../')) { const numDirsBack = argscmd.split('../').length - 1; newPWD = RemoveLastDirectoryPartOf(userPWD, numDirsBack); } userWorkingDirectories.set(sshSurfID, newPWD); await interaction.editReply(`Directory changed to: ${newPWD}`); return; } const execResponse = await makeApiRequest('/exec', apiToken, interaction, 'post', { cmd: command, pwd: userPWD }); if (execResponse.stdout.length > 2020) { const tempFilePath = '/tmp/paste'; const pasteCommand = `Command: ${command} | Container Owner: ${interaction.user.username}\n${execResponse.stdout}`; fs.writeFileSync(tempFilePath, pasteCommand, (err) => { if (err) { console.error(err); return; } }); const pasteout = await cmd("sleep 2; cat /tmp/paste | dpaste"); const mainEmbed = new EmbedBuilder() .setColor('#0099ff') .setTitle(`Container Owner: ${interaction.user.username}`) .setDescription(`The command: ${command} was too large for Discord.`) .addFields({ name: 'Please check the below output log:', value: pasteout.stdout.replace("Pro tip: you can password protect your paste just by typing a username and password after your paste command.", "") .replace("Paste Saved: ", "") .replace("-------------------------------------------------------", "") }) .setFooter({ text: `Requested by ${interaction.user.username}`, iconURL: `${interaction.user.displayAvatarURL()}` }); await interaction.editReply({ embeds: [mainEmbed] }); } else { let replyMessage = `\`\`\`\n${execResponse.stdout || 'No output'}\n\`\`\``; if (execResponse.stderr && execResponse.stderr.trim()) { replyMessage += `\n**Error:**\n\`\`\`\n${execResponse.stderr}\n\`\`\``; } await interaction.editReply(replyMessage); } break; 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, isEphemeral(interaction.user.id)); break; default: interaction.reply({ content: 'Command not recognized.', ephemeral: isEphemeral(interaction.user.id) }); break; } } catch (error) { console.error('Command error:', error); sendSexyEmbed('Error', 'An error occurred while processing your request.', interaction, isEphemeral(interaction.user.id)); } }); // Log in to Discord client.login(config.token);