From 1bf2e2f352728576161d982648f00d0b074b5eb3 Mon Sep 17 00:00:00 2001 From: dlinux-host Date: Wed, 2 Oct 2024 19:58:53 -0400 Subject: [PATCH] Add privacy controls --- user-bot.js | 361 +++++++++++++++++++++++----------------------- user_privacy.json | 3 + 2 files changed, 185 insertions(+), 179 deletions(-) create mode 100644 user_privacy.json diff --git a/user-bot.js b/user-bot.js index 405c55d..e90198a 100644 --- a/user-bot.js +++ b/user-bot.js @@ -10,6 +10,7 @@ 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 @@ -38,6 +39,24 @@ 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 @@ -123,6 +142,24 @@ function sendSexyEmbedWithFields(title, fields, interaction, ephemeral = false) }); } +// 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'), @@ -140,6 +177,7 @@ const commands = [ .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 @@ -178,9 +216,11 @@ const rest = new REST({ version: '10' }).setToken(config.token); // 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(); + // 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) => { @@ -205,240 +245,203 @@ client.on('interactionCreate', async interaction => { return sendSexyEmbed("Error", "User not found in the database.", 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); + 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); + ], 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); + ], 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); + ], 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); + ], interaction, isEphemeral(interaction.user.id)); 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 '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); + ], 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); + ], 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, true); + ], 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, true); + ], 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, true); + ], interaction, isEphemeral(interaction.user.id)); 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; + + 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 { - // Relative path - newPWD = `${userPWD}/${argscmd}`; + await interaction.editReply(`Already at the root directory: ${userPWD}`); } - - // 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 '/' - }); - - // Handle the command output - if (execResponse.stdout.length > 2020) { - // If the output is too large, save it to a temporary file and upload to a paste service - - - 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"); - - // Create an embed with the paste link - 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 { - // If the output is small enough, format it 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); + + if (argscmd === '~') { + userPWD = '/root'; + userWorkingDirectories.set(sshSurfID, userPWD); + await interaction.editReply(`Directory changed to: ${userPWD}`); + return; } - break; + + 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; } - - // 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('/') || '/'; + + 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', { @@ -447,18 +450,18 @@ client.on('interactionCreate', async interaction => { sendSexyEmbedWithFields('Notification', [ { name: 'Status', value: 'Success' }, { name: 'Message', value: notifyResponse.message } - ], interaction, true); + ], interaction, isEphemeral(interaction.user.id)); break; - + default: - interaction.reply('Command not recognized.'); + 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); + sendSexyEmbed('Error', 'An error occurred while processing your request.', interaction, isEphemeral(interaction.user.id)); } -}); +}); // Log in to Discord client.login(config.token); diff --git a/user_privacy.json b/user_privacy.json new file mode 100644 index 0000000..891856f --- /dev/null +++ b/user_privacy.json @@ -0,0 +1,3 @@ +{ + "342128351638585344": false +}