Skip to content

Commit

Permalink
Merge pull request #226 from IgorKowalczyk/cwd
Browse files Browse the repository at this point in the history
Add cwd memory, add development mode, add one-file config (v3)
  • Loading branch information
IgorKowalczyk authored Dec 24, 2023
2 parents dfee75e + 377d57d commit 08d1638
Show file tree
Hide file tree
Showing 12 changed files with 522 additions and 195 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copy this file to .env and fill in the values.

CHANNEL_ID="Discord channel ID"
OWNERS_IDS="ID 1,ID 2,ID 3"
TOKEN="Discord bot token"
CUSTOM_CWD="Default path to the bot's working directory (optional - remove this line if you don't need it)"
57 changes: 29 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,50 +29,51 @@ I can also use it to run commands on my local machine.
## 📦 Installation

1. Clone the repo `git clone /~https://github.com/igorkowalczyk/discord-ssh.git`
2. Install dependencies `npm install` or `pnpm install`
3. Create `.env` file and fill it with your data (see [`.env` config](#-env-config))
4. Run the bot `npm run start` or `pnpm run start`
5. Invite the bot to your server
6. Send command in channel which you set in `.env` file
7. Wait for the response, that's it!

> [!WARNING]
2. Install dependencies `pnpm install` or `npm install`
3. Create `.env` file in the root directory
4. Copy the content from [`.env` config](#-env-config)
5. Fill the `.env` file with your data
6. Run the bot `pnpm run start` or `npm run start` (or `pnpm run dev` or `npm run dev` for development)
7. Invite the bot to your server (see [Discord Developer Portal](https://discord.com/developers/applications))
8. Send command in channel which you set in `.env` file
9. Wait for the response, that's it!

> [!IMPORTANT]
> You have to enable `Message Content` intent in your [Discord Developer Portal](https://discord.com/developers/applications) to use this bot!
> [!NOTE]
> Bot will not respond to messages in other channels or other members than you (bot owner) unless you change it in the `.env` file or in the code
## 🔩 Limitations

- `sudo` commands are not supported
- Text inputs are not supported (e.g. `nano`)
- `cd` command is partially supported (you can change default directory in `.env`)
- Colored output is not supported and can be broken
- `sudo` / `su`commands are not supported, and probably never will be (for security reasons).
- Text inputs are not supported (e.g. `nano`), but you can use `echo` to create/edit files.
- Colored output is not supported and can be broken!

> [!NOTE]
> Changing directory (`cd`) is supported when it's at the beginning of a command
> Changing directory (`cd`) is supported when it's at the beginning of a command!
## 🔐 `.env` config

```
CHANNEL_ID=CHANNEL_ID_TO_RUN_SSH_COMMANDS
OWNER_ID=BOT_OWNER_ID
TOKEN=DISCORD_BOT_TOKEN
CUSTOM_CWD=DEFAULT_SSH_DIR_PATH
```
```sh
# Copy this file to .env and fill in the values.

| Variable | Description | Required |
| ------------ | --------------------------------------------- | -------- |
| `CHANNEL_ID` | Channel ID where bot will listen for commands | `true` |
| `OWNER_ID` | Discord user ID who can use the bot | `true` |
| `TOKEN` | Discord bot token | `true` |
| `CUSTOM_CWD` | Default directory for SSH commands | `false` |
CHANNEL_ID="Discord channel ID"
OWNERS_IDS="ID 1,ID 2,ID 3"
TOKEN="Discord bot token"
CUSTOM_CWD="Default path to the bot's working directory (optional - remove this line if you don't need it)"

```

> [!WARNING]
> The `CUSTOM_CWD` variable defaults to the directory where the bot is running!
| Variable | Description | Required |
| ------------ | ------------------------------------------------- | -------- |
| `CHANNEL_ID` | Channel ID where bot will listen for commands | `✅ Yes` |
| `OWNERS_IDS` | Users IDs who can use the bot (separated by `,`) | `✅ Yes` |
| `TOKEN` | Discord bot token | `✅ Yes` |
| `CUSTOM_CWD` | Default directory for SSH commands (Default: `/`) | `❌ No` |

> [!NOTE]
> You can get your Discord user ID by enabling `Developer Mode` in Discord settings and right-clicking on your profile
> You can get your Discord user ID/Cannel ID by enabling `Developer Mode` in Discord settings and right-clicking on your profile or channel
## ⁉️ Issues

Expand Down
18 changes: 18 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const defaultConfig = {
channel: process.env.CHANNEL_ID, // Channel ID
token: process.env.TOKEN, // Discord bot token
owners: [...process.env.OWNERS_IDS.split(",")], // Array of owners IDs (separated by commas)
embedColor: "#5865f2", // Discord's blurple
emojis: {
loading: "<a:loading:895227261752582154>", // https://cdn.discordapp.com/emojis/895227261752582154.gif?v=1
output: "📤",
error: "❌",
change: "↪️",
},
debugger: {
changeDir: true, // Displays the directory change in the terminal
showCommand: true, // Displays the command run in the terminal
moritoringUpdates: false, // Displays the monitoring updates in the terminal (every 5 seconds)
displayEventList: false, // Displays the event list in the terminal
},
};
29 changes: 29 additions & 0 deletions events/client/ready.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ActivityType } from "discord.js";
import { defaultConfig } from "../../config.js";
import { logger } from "../../utils/logger.js";

/**
* Handles the ready event.
*
* @param {object} client The Discord client.
* @returns {Promise<void>}
*/
export async function ready(client) {
try {
defaultConfig.channel = client.channels.cache.get(defaultConfig.channel);
if (!defaultConfig.channel) return logger("error", "Channel not found! Please check your CHANNEL_ID .env variable.");

if (!(await defaultConfig.channel.fetchWebhooks()).size) {
await defaultConfig.channel.createWebhook({
name: "SSH",
avatar: client.user.displayAvatarURL(),
reason: "SSH Webhook",
});
}

logger("ready", `Logged in as ${client.user.tag}! (ID: ${client.user.id})`);
client.user.setActivity("all ports!", { type: ActivityType.Watching });
} catch (error) {
logger("error", error);
}
}
70 changes: 70 additions & 0 deletions events/guild/messageCreate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from "node:fs";
import path from "node:path";
import { EmbedBuilder } from "discord.js";
import { defaultConfig } from "../../config.js";
import { execCommand } from "../../utils/execCommand.js";
import { logger } from "../../utils/logger.js";

/**
* Creates an embed.
*
* @param {string} description The embed description.
* @param {string} color The embed color.
* @returns {EmbedBuilder} The embed.
*/
function createEmbed(description, color) {
return new EmbedBuilder() // prettier
.setDescription(description)
.setColor(color);
}

/**
* Handles the messageCreate event.
*
* @param {object} client The Discord client.
* @param {object} message The message object.
* @returns {Promise<void>}
*/
export async function messageCreate(client, message) {
try {
if (message.author.bot) return;
if (message.channel !== defaultConfig.channel && !defaultConfig.owners.includes(message.author.id)) return;
if (!message.content) return;

const [command, ...args] = message.content.split(" ");

if (command === "cd") {
const newCWD = args.join(" ");
if (!newCWD) return;

const resolvedPath = path.resolve(client.customCWD, newCWD);
if (!fs.existsSync(resolvedPath)) {
const error = createEmbed(`${defaultConfig.emojis.error} **Directory does not exist**`, defaultConfig.embedColor);
return message.reply({ embeds: [error] });
}

try {
process.chdir(resolvedPath);
defaultConfig.debugger.changeDir && logger("event", `Changed directory from ${client.customCWD} to ${resolvedPath}`);

const changedDirectory = createEmbed(`${defaultConfig.emojis.change} **Changed directory from \`${client.customCWD}\` to \`${resolvedPath}\`**`, defaultConfig.embedColor);

client.customCWD = resolvedPath;
return message.reply({ embeds: [changedDirectory] });
} catch (err) {
defaultConfig.debugger.changeDir && logger("error", err);
const error = createEmbed(`${defaultConfig.emojis.error} **${err.message}**`, defaultConfig.embedColor);

return message.reply({ embeds: [error] });
}
}

const wait = createEmbed(`${defaultConfig.emojis.loading} **Waiting for server response...**`, defaultConfig.embedColor);

await message.reply({ embeds: [wait] });

await execCommand(client, message.content);
} catch (error) {
logger("error", error);
}
}
160 changes: 27 additions & 133 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,144 +1,38 @@
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import chalk from "chalk";
import { EmbedBuilder, Client, GatewayIntentBits, Events, ActivityType } from "discord.js";
import stripAnsi from "strip-ansi";
import { cpuTemperature, currentLoad, mem } from "systeminformation";
import "dotenv/config";
console.log(chalk.cyan(chalk.bold("[DISCORD] > Starting SSH...")));

const client = new Client({
allowedMentions: {
parse: ["users", "roles"],
repliedUser: false,
},
presence: {
status: "online",
afk: false,
},
intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMembers | GatewayIntentBits.GuildPresences | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
});

client.config = {
channel: process.env.CHANNEL_ID,
owner: process.env.OWNER_ID,
token: process.env.TOKEN,
cwd: process.env.CUSTOM_CWD,
};

EventEmitter.prototype._maxListeners = 100;

// Cache stats to eliminate "lag" on command
setInterval(() => {
cpuTemperature().then((data) => {
client.cpuTemperature = data.main;
});

currentLoad().then((data) => {
client.cpuUsage = data.currentLoad.toFixed(2);
});

mem().then((data) => {
const total = (data.total / 1048576).toFixed(2);
const used = (data.used / 1048576).toFixed(2);
client.memoryPercentage = ((used * 100) / total).toFixed(2);
import { Client, GatewayIntentBits } from "discord.js";
import loadEvents from "./utils/loadEvents.js";
import { logger } from "./utils/logger.js";

logger("event", "Starting SSH Bot session...");
logger("info", `Running version v${process.env.npm_package_version} on Node.js ${process.version} on ${process.platform} ${process.arch}`);
logger("info", "Check out the source code at /~https://github.com/igorkowalczyk/discord-ssh!");
logger("info", "Don't forget to star the repository, it helps a lot!");

try {
const client = new Client({
allowedMentions: {
parse: ["users", "roles"],
repliedUser: false,
},
intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMembers | GatewayIntentBits.GuildPresences | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
});
}, 5000);

async function exec(input, options, customCWD) {
if (options?.terminal)
await (await client.config.channel.fetchWebhooks()).first().send(input, {
username: client.config.channel.guild.members.cache.get(client.config.owner.id)?.nickname || client.config.owner.username,
avatarURL: client.config.owner.displayAvatarURL({ format: "png" }),
});
let output = "";
const args = input.split(" ");
const command = args.shift();
const cmd = spawn(`${command}`, args, {
shell: true,
env: { COLUMNS: 128 },
cwd: customCWD || client.config.cwd || process.cwd(),
});
client.customCWD = process.env.CUSTOM_CWD || process.cwd();

cmd.stdout.on("data", (data) => {
output += data;
});
cmd.stderr.on("data", (data) => {
output += data;
});
cmd.on("exit", async () => {
if (output) {
//await client.config.channel.bulkDelete(1);
const chunkStr = (str, n, acc) => {
if (str.length === 0) {
return acc;
} else {
acc.push(str.substring(0, n));
return chunkStr(str.substring(n), n, acc);
}
};
const outputDiscord = chunkStr(output, 4000, []);
logger("info", "Loading events...");
await loadEvents(client);

const embed = new EmbedBuilder().setColor("#4f545c").setTitle("📤 Output").setTimestamp();
let i = 0;
outputDiscord.forEach((item) => {
i++;
embed.setFooter({ text: `Page ${i}/${outputDiscord.length}`, icon: client.user.displayAvatarURL() });
embed.setDescription(`\`\`\`${stripAnsi(item, true) || "No output!"}\`\`\``);
if (i == outputDiscord.length) embed.addFields([{ name: "\u200B", value: `\`\`\`CWD: ${customCWD}\nCPU: ${client.cpuUsage}% | RAM: ${client.memoryPercentage}% | Temp: ${client.cpuTemperature}°C\`\`\`` }]);
const finalMessage = client.config.channel.messages.cache.first();
if (i !== 1) {
client.config.channel.send({ embeds: [embed] });
} else {
finalMessage.reply({ embeds: [embed] });
}
});
}
});
logger("info", "Logging in...");
await client.login(process.env.TOKEN);
} catch (error) {
logger("error", error);
process.exit(1);
}

client.on(Events.MessageCreate, (msg) => {
if (msg.author.bot) return;
if (msg.channel === client.config.channel && msg.author === client.config.owner) {
if (msg.content.startsWith("cd")) {
const cd = new EmbedBuilder().setDescription("↪️ **Changed directory from `" + client.config.cwd + "` to `" + msg.content.split(" ")[1] + "`**\n\n<a:loading:895227261752582154> **Waiting for server response...**").setColor("#5865f2");
msg.reply({ embeds: [cd] });
exec(msg.content.split(" ").slice(2).join(" "), null, msg.content.split(" ")[1].toString());
} else {
const wait = new EmbedBuilder().setDescription("<a:loading:895227261752582154> **Waiting for server response...**").setColor("#5865f2");
msg.reply({ embeds: [wait] });
exec(msg.content, null, client.config.cwd);
}
}
});

client.once(Events.ClientReady, async () => {
client.config.channel = client.channels.cache.get(client.config.channel);
if (!client.config.channel) {
throw new Error("Invalid CHANNEL_ID in .env!");
}
client.config.owner = await client.users.fetch(client.config.owner);
if (!client.config.owner) {
throw new Error("Invalid OWNER_ID in .env!");
}

if (!(await client.config.channel.fetchWebhooks()).size) await client.config.channel.createWebhook(client.config.owner.tag, { avatar: client.config.owner.displayAvatarURL({ format: "png" }) });
console.log(chalk.cyan(chalk.bold(`[DISCORD] > Logged in as ${client.user.tag}`)));
client.user.setActivity("all ports!", { type: ActivityType.Watching });
});

process.stdin.on("data", (data) => exec(data.toString(), { terminal: true }));

client.login(client.config.token).catch(() => {
throw new Error("Invalid TOKEN in .env");
});

process.on("unhandledRejection", async (reason) => {
return console.log(chalk.red(chalk.bold(`[ERROR] > Unhandled Rejection: ${reason}`)));
return logger("error", reason);
});

process.on("uncaughtException", async (err) => {
return console.log(chalk.red(chalk.bold(`[ERROR] > Uncaught Exception: ${err}`)));
});
process.on("uncaughtExceptionMonitor", async (err) => {
return console.log(chalk.red(chalk.bold(`[ERROR] > Uncaught Exception Monitor: ${err}`)));
return logger("error", err);
});
Loading

0 comments on commit 08d1638

Please sign in to comment.