Compare commits

..

No commits in common. "32e8d37aecd0020c4649f82199862b4a4c5403ae" and "440fd75f1315ffdbda3b29116061da4781618963" have entirely different histories.

3 changed files with 183 additions and 190 deletions

3
.gitignore vendored
View File

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

View File

@ -10,7 +10,6 @@ let sshSurfID; // Variable to store the user ID from the database
// Paths to config and tokens files // Paths to config and tokens files
const tokensFile = './tokens.json'; const tokensFile = './tokens.json';
const privacyFile = './user_privacy.json'; // Privacy file path
const config = JSON.parse(readFileSync('./config.json', 'utf8')); const config = JSON.parse(readFileSync('./config.json', 'utf8'));
// MySQL connection // MySQL connection
@ -39,24 +38,6 @@ function saveTokens(tokens) {
jsonfile.writeFileSync(tokensFile, tokens, { spaces: 2 }); 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 // Automatically request a new token if it doesn't exist or is invalid
async function fetchAndSaveToken(sshSurfID, interaction) { async function fetchAndSaveToken(sshSurfID, interaction) {
return unirest return unirest
@ -142,24 +123,6 @@ 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 // Slash command definitions
const commands = [ const commands = [
new SlashCommandBuilder().setName('hello').setDescription('Say hello via API'), new SlashCommandBuilder().setName('hello').setDescription('Say hello via API'),
@ -177,7 +140,6 @@ const commands = [
.addStringOption(option => option.setName('command').setDescription('Command to execute').setRequired(true)), .addStringOption(option => option.setName('command').setDescription('Command to execute').setRequired(true)),
new SlashCommandBuilder().setName('notify').setDescription('Send a notification to Discord') new SlashCommandBuilder().setName('notify').setDescription('Send a notification to Discord')
.addStringOption(option => option.setName('message').setDescription('Message to send').setRequired(true)), .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 // Register commands with Discord
@ -216,11 +178,9 @@ const rest = new REST({ version: '10' }).setToken(config.token);
// Handle bot interactions // Handle bot interactions
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return; if (!interaction.isCommand()) return;
// Check if the command is /privacy, always make it ephemeral
const isPrivacyCommand = interaction.commandName === 'privacy'; // Defer the reply to allow time for the command to run
await interaction.deferReply();
// 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 // First, we fetch the sshSurfID from the database using interaction.user.id
let sshSurfID = await new Promise((resolve, reject) => { let sshSurfID = await new Promise((resolve, reject) => {
@ -245,203 +205,240 @@ client.on('interactionCreate', async interaction => {
return sendSexyEmbed("Error", "User not found in the database.", 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 // Once sshSurfID is set, we proceed with token fetching and API requests
const apiToken = await getToken(sshSurfID, interaction); const apiToken = await getToken(sshSurfID, interaction);
try { try {
switch (interaction.commandName) { switch (interaction.commandName) {
case 'hello': case 'hello':
const helloResponse = await makeApiRequest('/hello', apiToken, interaction); const helloResponse = await makeApiRequest('/hello', apiToken, interaction);
sendSexyEmbed("Hello", `Message: ${helloResponse.message}`, interaction, isEphemeral(interaction.user.id)); sendSexyEmbed("Hello", `Message: ${helloResponse.message}`, interaction);
break; break;
case 'name': case 'name':
const nameResponse = await makeApiRequest('/name', apiToken, interaction); const nameResponse = await makeApiRequest('/name', apiToken, interaction);
sendSexyEmbedWithFields('Username', [ sendSexyEmbedWithFields('Username', [
{ name: 'Username', value: nameResponse.message } { name: 'Username', value: nameResponse.message }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'start': case 'start':
const startResponse = await makeApiRequest('/start', apiToken, interaction); const startResponse = await makeApiRequest('/start', apiToken, interaction);
sendSexyEmbedWithFields('Start Server', [ sendSexyEmbedWithFields('Start Server', [
{ name: 'Status', value: 'Success' }, { name: 'Status', value: 'Success' },
{ name: 'Message', value: startResponse.message } { name: 'Message', value: startResponse.message }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'stop': case 'stop':
const stopResponse = await makeApiRequest('/stop', apiToken, interaction); const stopResponse = await makeApiRequest('/stop', apiToken, interaction);
sendSexyEmbedWithFields('Stop Server', [ sendSexyEmbedWithFields('Stop Server', [
{ name: 'Status', value: 'Success' }, { name: 'Status', value: 'Success' },
{ name: 'Message', value: stopResponse.message } { name: 'Message', value: stopResponse.message }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'restart': case 'restart':
const restartResponse = await makeApiRequest('/restart', apiToken, interaction); const restartResponse = await makeApiRequest('/restart', apiToken, interaction);
sendSexyEmbedWithFields('Restart Server', [ sendSexyEmbedWithFields('Restart Server', [
{ name: 'Status', value: 'Success' }, { name: 'Status', value: 'Success' },
{ name: 'Message', value: restartResponse.message } { name: 'Message', value: restartResponse.message }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'info': case 'info':
const infoResponse = await makeApiRequest('/info', apiToken, interaction); const infoResponse = await makeApiRequest('/info', apiToken, interaction);
const containerName = infoResponse.data?.name || 'N/A';
const ipAddress = infoResponse.data?.IPAddress || 'N/A'; // Extract and fallback data fields from the response
const macAddress = infoResponse.data?.MacAddress || 'N/A'; const containerName = infoResponse.data?.name || 'N/A';
const memory = infoResponse.data?.memory || 'N/A'; const ipAddress = infoResponse.data?.IPAddress || 'N/A';
const cpus = infoResponse.data?.cpus || 'N/A'; const macAddress = infoResponse.data?.MacAddress || 'N/A';
const restartPolicy = infoResponse.data?.restartPolicy?.Name || 'N/A'; const memory = infoResponse.data?.memory || 'N/A';
const restarts = infoResponse.data?.restarts !== undefined ? infoResponse.data.restarts : 'N/A'; const cpus = infoResponse.data?.cpus || 'N/A';
const status = infoResponse.data?.state?.Status || 'Unknown'; const restartPolicy = infoResponse.data?.restartPolicy?.Name || 'N/A';
const pid = infoResponse.data?.state?.Pid || 'N/A'; const restarts = infoResponse.data?.restarts !== undefined ? infoResponse.data.restarts : 'N/A';
const startedAt = infoResponse.data?.state?.StartedAt || 'N/A'; const status = infoResponse.data?.state?.Status || 'Unknown';
const image = infoResponse.data?.image || 'N/A'; const pid = infoResponse.data?.state?.Pid || 'N/A';
const createdAt = infoResponse.data?.created || 'N/A'; const startedAt = infoResponse.data?.state?.StartedAt || 'N/A';
const image = infoResponse.data?.image || 'N/A';
sendSexyEmbedWithFields('Container Info', [ const createdAt = infoResponse.data?.created || 'N/A';
{ name: 'Name', value: containerName },
{ name: 'IP Address', value: ipAddress }, // Format and send the embed
{ name: 'MAC Address', value: macAddress }, sendSexyEmbedWithFields('Container Info', [
{ name: 'Memory', value: memory }, { name: 'Name', value: containerName },
{ name: 'CPUs', value: cpus }, { name: 'IP Address', value: ipAddress },
{ name: 'Restart Policy', value: restartPolicy }, { name: 'MAC Address', value: macAddress },
{ name: 'Restarts', value: `${restarts}` }, { name: 'Memory', value: memory },
{ name: 'Status', value: status }, { name: 'CPUs', value: cpus },
{ name: 'PID', value: `${pid}` }, { name: 'Restart Policy', value: restartPolicy },
{ name: 'Started At', value: startedAt }, { name: 'Restarts', value: `${restarts}` },
{ name: 'Created At', value: createdAt } { name: 'Status', value: status },
], interaction, isEphemeral(interaction.user.id)); { name: 'PID', value: `${pid}` },
break; { name: 'Started At', value: startedAt },
{ name: 'Created At', value: createdAt }
], interaction);
break;
case 'stats': case 'stats':
const statsResponse = await makeApiRequest('/stats', apiToken, interaction); const statsResponse = await makeApiRequest('/stats', apiToken, interaction);
sendSexyEmbedWithFields('Container Stats', [ sendSexyEmbedWithFields('Container Stats', [
{ name: 'Memory Usage', value: `${statsResponse.data.memory.raw} (${statsResponse.data.memory.percent})` }, { name: 'Memory Usage', value: `${statsResponse.data.memory.raw} (${statsResponse.data.memory.percent})` },
{ name: 'CPU Usage', value: statsResponse.data.cpu } { name: 'CPU Usage', value: statsResponse.data.cpu }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'time': case 'time':
const timeResponse = await makeApiRequest('/time', apiToken, interaction); const timeResponse = await makeApiRequest('/time', apiToken, interaction);
sendSexyEmbedWithFields('Container Expire Time', [ sendSexyEmbedWithFields('Container Expire Time', [
{ name: 'Expire Date', value: timeResponse.expireDate } { name: 'Expire Date', value: timeResponse.expireDate }
], interaction, isEphemeral(interaction.user.id)); ], interaction);
break; break;
case 'root-password': case 'root-password':
const rootPassResponse = await makeApiRequest('/rootpass', apiToken, interaction); const rootPassResponse = await makeApiRequest('/rootpass', apiToken, interaction);
sendSexyEmbedWithFields('Root Password', [ sendSexyEmbedWithFields('Root Password', [
{ name: 'New Root Password', value: rootPassResponse.newRootPass } { name: 'New Root Password', value: rootPassResponse.newRootPass }
], interaction, isEphemeral(interaction.user.id)); ], interaction, true);
break; break;
case 'new-api-key': case 'new-api-key':
const newKeyResponse = await makeApiRequest('/new-key', apiToken, interaction); const newKeyResponse = await makeApiRequest('/new-key', apiToken, interaction);
sendSexyEmbedWithFields('New API Key', [ sendSexyEmbedWithFields('New API Key', [
{ name: 'New API Key', value: newKeyResponse.newAPIKey } { name: 'New API Key', value: newKeyResponse.newAPIKey }
], interaction, isEphemeral(interaction.user.id)); ], interaction, true);
break; break;
case 'key-expire-time': case 'key-expire-time':
const keyTimeResponse = await makeApiRequest('/key-time', apiToken, interaction); const keyTimeResponse = await makeApiRequest('/key-time', apiToken, interaction);
sendSexyEmbedWithFields('API Key Expire Time', [ sendSexyEmbedWithFields('API Key Expire Time', [
{ name: 'Expire Date', value: keyTimeResponse.expireDate } { name: 'Expire Date', value: keyTimeResponse.expireDate }
], interaction, isEphemeral(interaction.user.id)); ], interaction, true);
break; break;
case 'x':
const command = interaction.options.getString('command');
let userPWD = userWorkingDirectories.get(sshSurfID) || '/'; case 'x': {
if (command.startsWith('cd')) { const command = interaction.options.getString('command');
let argscmd = command.replace('cd ', '').trim();
if (argscmd === '..') { // Get the user's current working directory or default to root (/)
if (userPWD !== '/') { let userPWD = userWorkingDirectories.get(sshSurfID) || '/';
const newPWD = userPWD.split('/').slice(0, -1).join('/') || '/';
userPWD = newPWD; // Handle 'cd' command logic
userWorkingDirectories.set(sshSurfID, newPWD); if (command.startsWith('cd')) {
await interaction.editReply(`Directory changed to: ${newPWD}`); let argscmd = command.replace('cd ', '').trim();
} else {
await interaction.editReply(`Already at the root directory: ${userPWD}`); // Handle 'cd ..' for going up one directory
} if (argscmd === '..') {
return; if (userPWD !== '/') {
} // Remove the last part of the current path
const newPWD = userPWD.split('/').slice(0, -1).join('/') || '/';
if (argscmd === '~') { userPWD = newPWD;
userPWD = '/root'; userWorkingDirectories.set(sshSurfID, newPWD);
userWorkingDirectories.set(sshSurfID, userPWD); await interaction.editReply(`Directory changed to: ${newPWD}`);
await interaction.editReply(`Directory changed to: ${userPWD}`); } else {
return; await interaction.editReply(`Already at the root directory: ${userPWD}`);
} }
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; return;
} }
});
// Handle '~' for home directory
const pasteout = await cmd("sleep 2; cat /tmp/paste | dpaste"); if (argscmd === '~') {
userPWD = '/root';
const mainEmbed = new EmbedBuilder() userWorkingDirectories.set(sshSurfID, userPWD);
.setColor('#0099ff') await interaction.editReply(`Directory changed to: ${userPWD}`);
.setTitle(`Container Owner: ${interaction.user.username}`) return;
.setDescription(`The command: ${command} was too large for Discord.`) }
.addFields({
name: 'Please check the below output log:', // Handle absolute and relative paths
value: pasteout.stdout.replace("Pro tip: you can password protect your paste just by typing a username and password after your paste command.", "") let newPWD;
.replace("Paste Saved: ", "") if (argscmd.startsWith('/')) {
.replace("-------------------------------------------------------", "") // Absolute path
}) newPWD = argscmd;
.setFooter({ text: `Requested by ${interaction.user.username}`, iconURL: `${interaction.user.displayAvatarURL()}` }); } else {
// Relative path
await interaction.editReply({ embeds: [mainEmbed] }); newPWD = `${userPWD}/${argscmd}`;
}
} else {
let replyMessage = `\`\`\`\n${execResponse.stdout || 'No output'}\n\`\`\``; // Normalize the path (remove extra slashes)
newPWD = newPWD.replace(/([^:]\/)\/+/g, "$1");
if (execResponse.stderr && execResponse.stderr.trim()) {
replyMessage += `\n**Error:**\n\`\`\`\n${execResponse.stderr}\n\`\`\``; // 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;
} }
await interaction.editReply(replyMessage); // 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);
}
break;
} }
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': case 'notify':
const message = interaction.options.getString('message'); const message = interaction.options.getString('message');
const notifyResponse = await makeApiRequest('/notify', apiToken, interaction, 'post', { const notifyResponse = await makeApiRequest('/notify', apiToken, interaction, 'post', {
@ -450,18 +447,18 @@ client.on('interactionCreate', async interaction => {
sendSexyEmbedWithFields('Notification', [ sendSexyEmbedWithFields('Notification', [
{ name: 'Status', value: 'Success' }, { name: 'Status', value: 'Success' },
{ name: 'Message', value: notifyResponse.message } { name: 'Message', value: notifyResponse.message }
], interaction, isEphemeral(interaction.user.id)); ], interaction, true);
break; break;
default: default:
interaction.reply({ content: 'Command not recognized.', ephemeral: isEphemeral(interaction.user.id) }); interaction.reply('Command not recognized.');
break; break;
} }
} catch (error) { } catch (error) {
console.error('Command error:', error); console.error('Command error:', error);
sendSexyEmbed('Error', 'An error occurred while processing your request.', interaction, isEphemeral(interaction.user.id)); sendSexyEmbed('Error', 'An error occurred while processing your request.', interaction);
} }
}); });
// Log in to Discord // Log in to Discord
client.login(config.token); client.login(config.token);

View File

@ -1,3 +0,0 @@
{
"342128351638585344": false
}