Skip to content

Commit

Permalink
Move functions to files, clean and refactor code, update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
IgorKowalczyk committed Dec 24, 2023
1 parent 2d2f8e0 commit 011083f
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 184 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)"
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ 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 (see [Discord Developer Portal](https://discord.com/developers/applications))
6. Send command in channel which you set in `.env` file
7. Wait for the response, that's it!
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!
Expand All @@ -44,31 +46,31 @@ I can also use it to run commands on my local machine.
## 🔩 Limitations

- `sudo` commands are not supported
- Text inputs are not supported (e.g. `nano`)
- `sudo` 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 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!
## 🔐 `.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` |
| `OWNERS_IDS` | Users IDs who can use the bot (separated by `,`) | `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 | `true` |
| `OWNERS_IDS` | Users IDs who can use the bot (separated by `,`) | `true` |
| `TOKEN` | Discord bot token | `true` |
| `CUSTOM_CWD` | Default directory for SSH commands (Default: `/`) | `false` |

> [!NOTE]
> You can get your Discord user ID by enabling `Developer Mode` in Discord settings and right-clicking on your profile
Expand Down
16 changes: 11 additions & 5 deletions config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
export const defaultConfig = {
channel: process.env.CHANNEL_ID,
token: process.env.TOKEN,
embedColor: "#5865f2",
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>",
loading: "<a:loading:895227261752582154>", // https://cdn.discordapp.com/emojis/895227261752582154.gif?v=1
output: "📤",
error: "❌",
change: "↪️",
},
owners: [...process.env.OWNERS_IDS.split(",")],
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);
}
}
164 changes: 26 additions & 138 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,150 +1,38 @@
import "dotenv/config";
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import path from "node:path";
import chalk from "chalk";
import { EmbedBuilder, Client, GatewayIntentBits, Events, ActivityType, codeBlock } from "discord.js";
import stripAnsi from "strip-ansi";
import { cpuTemperature as checkCpuTemperature, currentLoad, mem } from "systeminformation";
import { defaultConfig } from "./config.js";
console.log(chalk.cyan(chalk.bold("[DISCORD] > Starting SSH...")));

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

let customCWD = process.env.CUSTOM_CWD || process.cwd();

let monitoringData = {
cpuTemperature: 0,
cpuUsage: 0,
memoryPercentage: 0,
};

EventEmitter.prototype._maxListeners = 100;

const chunkString = (str, n, acc) => {
if (str.length === 0) return acc;

acc.push(str.substring(0, n));
return chunkString(str.substring(n), n, acc);
};

setInterval(async () => {
const cpuTemp = await checkCpuTemperature();
const cpuUsageTest = await currentLoad();
const memory = await mem();

monitoringData = {
cpuTemperature: cpuTemp.main,
cpuUsage: cpuUsageTest.currentLoad.toFixed(2),
memoryPercentage: ((memory.used / 1048576 / (memory.total / 1048576)) * 100).toFixed(2),
};
}, 5000);

async function exec(input) {
let output = "";

const args = input.split(" ");
const command = args.shift();

const cmd = spawn(`${command}`, args, {
shell: true,
env: { COLUMNS: 128 },
cwd: customCWD,
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,
});

cmd.stdout.on("data", (data) => (output += data));
cmd.stderr.on("data", (data) => (output += data));

cmd.on("exit", async () => {
const outputDiscord = chunkString(output || "", 3000, []);
client.customCWD = process.env.CUSTOM_CWD || process.cwd();

const embed = new EmbedBuilder() // prettier
.setColor("#4f545c")
.setTitle(`${defaultConfig.emojis.output} Output`)
.setTimestamp();
logger("info", "Loading events...");
await loadEvents(client);

outputDiscord.map((item, index) => {
const index2 = index + 1;
embed.setFooter({ text: `Page ${index2}/${outputDiscord.length}`, icon: client.user.displayAvatarURL() });
embed.setDescription(codeBlock(stripAnsi(item, true) || "No output!"));

if (index2 == outputDiscord.length) embed.setDescription(embed.data.description + `\n${codeBlock(`CWD: ${customCWD}\nCPU: ${monitoringData.cpuUsage}% | RAM: ${monitoringData.memoryPercentage}% | Temp: ${monitoringData.cpuTemperature}°C`)}`);

const finalMessage = defaultConfig.channel.messages.cache.first();
if (index2 !== 1) defaultConfig.channel.send({ embeds: [embed] });

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, async (message) => {
if (message.author.bot) return;
if (message.channel !== defaultConfig.channel && !defaultConfig.owners.includes(message.author.id)) return;
if (!message.content) return;

if (message.content.startsWith("cd")) {
const newCWD = message.content.split(" ")[1];
if (!newCWD) return;

try {
process.chdir(path.resolve(customCWD, newCWD));

const changedDirectory = new EmbedBuilder() // prettier
.setDescription(`${defaultConfig.emojis.change} **Changed directory from \`${customCWD}\` to \`${path.resolve(customCWD, newCWD)}\`**`)
.setColor(defaultConfig.embedColor);

customCWD = path.resolve(customCWD, newCWD);
return message.reply({ embeds: [changedDirectory] });
} catch (err) {
const error = new EmbedBuilder() // prettier
.setDescription(`${defaultConfig.emojis.error} **${err.message}**`)
.setColor(defaultConfig.embedColor);

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

const wait = new EmbedBuilder() // prettier
.setDescription(`${defaultConfig.emojis.loading} **Waiting for server response...**`)
.setColor(defaultConfig.embedColor);

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

await exec(message.content);
});

client.once(Events.ClientReady, async () => {
defaultConfig.channel = client.channels.cache.get(defaultConfig.channel);
if (!defaultConfig.channel) throw new Error("Invalid CHANNEL_ID in .env!");

if (!(await defaultConfig.channel.fetchWebhooks()).size) {
await defaultConfig.channel.createWebhook({
name: "SSH",
avatar: client.user.displayAvatarURL(),
reason: "SSH Webhook",
});
}
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(process.env.TOKEN).catch((error) => {
throw new Error(error);
});

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}`)));
return logger("error", err);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "discord-ssh",
"version": "2.0.1",
"version": "3.0.0",
"description": "Discord bot for using shell commands remotely through Discord",
"exports": null,
"type": "module",
Expand All @@ -16,6 +16,7 @@
"chalk": "5.3.0",
"discord.js": "14.14.1",
"dotenv": "16.3.1",
"globby": "14.0.0",
"strip-ansi": "7.1.0",
"systeminformation": "5.21.22"
},
Expand Down
Loading

0 comments on commit 011083f

Please sign in to comment.