Skip to content

Commit 08d1638

Browse files
Merge pull request #226 from IgorKowalczyk/cwd
Add cwd memory, add development mode, add one-file config (v3)
2 parents dfee75e + 377d57d commit 08d1638

File tree

12 files changed

+522
-195
lines changed

12 files changed

+522
-195
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copy this file to .env and fill in the values.
2+
3+
CHANNEL_ID="Discord channel ID"
4+
OWNERS_IDS="ID 1,ID 2,ID 3"
5+
TOKEN="Discord bot token"
6+
CUSTOM_CWD="Default path to the bot's working directory (optional - remove this line if you don't need it)"

README.md

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,50 +29,51 @@ I can also use it to run commands on my local machine.
2929
## 📦 Installation
3030

3131
1. Clone the repo `git clone https://github.com/igorkowalczyk/discord-ssh.git`
32-
2. Install dependencies `npm install` or `pnpm install`
33-
3. Create `.env` file and fill it with your data (see [`.env` config](#-env-config))
34-
4. Run the bot `npm run start` or `pnpm run start`
35-
5. Invite the bot to your server
36-
6. Send command in channel which you set in `.env` file
37-
7. Wait for the response, that's it!
38-
39-
> [!WARNING]
32+
2. Install dependencies `pnpm install` or `npm install`
33+
3. Create `.env` file in the root directory
34+
4. Copy the content from [`.env` config](#-env-config)
35+
5. Fill the `.env` file with your data
36+
6. Run the bot `pnpm run start` or `npm run start` (or `pnpm run dev` or `npm run dev` for development)
37+
7. Invite the bot to your server (see [Discord Developer Portal](https://discord.com/developers/applications))
38+
8. Send command in channel which you set in `.env` file
39+
9. Wait for the response, that's it!
40+
41+
> [!IMPORTANT]
4042
> You have to enable `Message Content` intent in your [Discord Developer Portal](https://discord.com/developers/applications) to use this bot!
4143
4244
> [!NOTE]
4345
> 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
4446
4547
## 🔩 Limitations
4648

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

5253
> [!NOTE]
53-
> Changing directory (`cd`) is supported when it's at the beginning of a command
54+
> Changing directory (`cd`) is supported when it's at the beginning of a command!
5455
5556
## 🔐 `.env` config
5657

57-
```
58-
CHANNEL_ID=CHANNEL_ID_TO_RUN_SSH_COMMANDS
59-
OWNER_ID=BOT_OWNER_ID
60-
TOKEN=DISCORD_BOT_TOKEN
61-
CUSTOM_CWD=DEFAULT_SSH_DIR_PATH
62-
```
58+
```sh
59+
# Copy this file to .env and fill in the values.
6360

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

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

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

config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const defaultConfig = {
2+
channel: process.env.CHANNEL_ID, // Channel ID
3+
token: process.env.TOKEN, // Discord bot token
4+
owners: [...process.env.OWNERS_IDS.split(",")], // Array of owners IDs (separated by commas)
5+
embedColor: "#5865f2", // Discord's blurple
6+
emojis: {
7+
loading: "<a:loading:895227261752582154>", // https://cdn.discordapp.com/emojis/895227261752582154.gif?v=1
8+
output: "📤",
9+
error: "❌",
10+
change: "↪️",
11+
},
12+
debugger: {
13+
changeDir: true, // Displays the directory change in the terminal
14+
showCommand: true, // Displays the command run in the terminal
15+
moritoringUpdates: false, // Displays the monitoring updates in the terminal (every 5 seconds)
16+
displayEventList: false, // Displays the event list in the terminal
17+
},
18+
};

events/client/ready.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ActivityType } from "discord.js";
2+
import { defaultConfig } from "../../config.js";
3+
import { logger } from "../../utils/logger.js";
4+
5+
/**
6+
* Handles the ready event.
7+
*
8+
* @param {object} client The Discord client.
9+
* @returns {Promise<void>}
10+
*/
11+
export async function ready(client) {
12+
try {
13+
defaultConfig.channel = client.channels.cache.get(defaultConfig.channel);
14+
if (!defaultConfig.channel) return logger("error", "Channel not found! Please check your CHANNEL_ID .env variable.");
15+
16+
if (!(await defaultConfig.channel.fetchWebhooks()).size) {
17+
await defaultConfig.channel.createWebhook({
18+
name: "SSH",
19+
avatar: client.user.displayAvatarURL(),
20+
reason: "SSH Webhook",
21+
});
22+
}
23+
24+
logger("ready", `Logged in as ${client.user.tag}! (ID: ${client.user.id})`);
25+
client.user.setActivity("all ports!", { type: ActivityType.Watching });
26+
} catch (error) {
27+
logger("error", error);
28+
}
29+
}

events/guild/messageCreate.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { EmbedBuilder } from "discord.js";
4+
import { defaultConfig } from "../../config.js";
5+
import { execCommand } from "../../utils/execCommand.js";
6+
import { logger } from "../../utils/logger.js";
7+
8+
/**
9+
* Creates an embed.
10+
*
11+
* @param {string} description The embed description.
12+
* @param {string} color The embed color.
13+
* @returns {EmbedBuilder} The embed.
14+
*/
15+
function createEmbed(description, color) {
16+
return new EmbedBuilder() // prettier
17+
.setDescription(description)
18+
.setColor(color);
19+
}
20+
21+
/**
22+
* Handles the messageCreate event.
23+
*
24+
* @param {object} client The Discord client.
25+
* @param {object} message The message object.
26+
* @returns {Promise<void>}
27+
*/
28+
export async function messageCreate(client, message) {
29+
try {
30+
if (message.author.bot) return;
31+
if (message.channel !== defaultConfig.channel && !defaultConfig.owners.includes(message.author.id)) return;
32+
if (!message.content) return;
33+
34+
const [command, ...args] = message.content.split(" ");
35+
36+
if (command === "cd") {
37+
const newCWD = args.join(" ");
38+
if (!newCWD) return;
39+
40+
const resolvedPath = path.resolve(client.customCWD, newCWD);
41+
if (!fs.existsSync(resolvedPath)) {
42+
const error = createEmbed(`${defaultConfig.emojis.error} **Directory does not exist**`, defaultConfig.embedColor);
43+
return message.reply({ embeds: [error] });
44+
}
45+
46+
try {
47+
process.chdir(resolvedPath);
48+
defaultConfig.debugger.changeDir && logger("event", `Changed directory from ${client.customCWD} to ${resolvedPath}`);
49+
50+
const changedDirectory = createEmbed(`${defaultConfig.emojis.change} **Changed directory from \`${client.customCWD}\` to \`${resolvedPath}\`**`, defaultConfig.embedColor);
51+
52+
client.customCWD = resolvedPath;
53+
return message.reply({ embeds: [changedDirectory] });
54+
} catch (err) {
55+
defaultConfig.debugger.changeDir && logger("error", err);
56+
const error = createEmbed(`${defaultConfig.emojis.error} **${err.message}**`, defaultConfig.embedColor);
57+
58+
return message.reply({ embeds: [error] });
59+
}
60+
}
61+
62+
const wait = createEmbed(`${defaultConfig.emojis.loading} **Waiting for server response...**`, defaultConfig.embedColor);
63+
64+
await message.reply({ embeds: [wait] });
65+
66+
await execCommand(client, message.content);
67+
} catch (error) {
68+
logger("error", error);
69+
}
70+
}

index.js

Lines changed: 27 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,38 @@
1-
import { spawn } from "node:child_process";
2-
import { EventEmitter } from "node:events";
3-
import chalk from "chalk";
4-
import { EmbedBuilder, Client, GatewayIntentBits, Events, ActivityType } from "discord.js";
5-
import stripAnsi from "strip-ansi";
6-
import { cpuTemperature, currentLoad, mem } from "systeminformation";
71
import "dotenv/config";
8-
console.log(chalk.cyan(chalk.bold("[DISCORD] > Starting SSH...")));
9-
10-
const client = new Client({
11-
allowedMentions: {
12-
parse: ["users", "roles"],
13-
repliedUser: false,
14-
},
15-
presence: {
16-
status: "online",
17-
afk: false,
18-
},
19-
intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMembers | GatewayIntentBits.GuildPresences | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
20-
});
21-
22-
client.config = {
23-
channel: process.env.CHANNEL_ID,
24-
owner: process.env.OWNER_ID,
25-
token: process.env.TOKEN,
26-
cwd: process.env.CUSTOM_CWD,
27-
};
28-
29-
EventEmitter.prototype._maxListeners = 100;
30-
31-
// Cache stats to eliminate "lag" on command
32-
setInterval(() => {
33-
cpuTemperature().then((data) => {
34-
client.cpuTemperature = data.main;
35-
});
36-
37-
currentLoad().then((data) => {
38-
client.cpuUsage = data.currentLoad.toFixed(2);
39-
});
40-
41-
mem().then((data) => {
42-
const total = (data.total / 1048576).toFixed(2);
43-
const used = (data.used / 1048576).toFixed(2);
44-
client.memoryPercentage = ((used * 100) / total).toFixed(2);
2+
import { Client, GatewayIntentBits } from "discord.js";
3+
import loadEvents from "./utils/loadEvents.js";
4+
import { logger } from "./utils/logger.js";
5+
6+
logger("event", "Starting SSH Bot session...");
7+
logger("info", `Running version v${process.env.npm_package_version} on Node.js ${process.version} on ${process.platform} ${process.arch}`);
8+
logger("info", "Check out the source code at https://github.com/igorkowalczyk/discord-ssh!");
9+
logger("info", "Don't forget to star the repository, it helps a lot!");
10+
11+
try {
12+
const client = new Client({
13+
allowedMentions: {
14+
parse: ["users", "roles"],
15+
repliedUser: false,
16+
},
17+
intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMembers | GatewayIntentBits.GuildPresences | GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent,
4518
});
46-
}, 5000);
4719

48-
async function exec(input, options, customCWD) {
49-
if (options?.terminal)
50-
await (await client.config.channel.fetchWebhooks()).first().send(input, {
51-
username: client.config.channel.guild.members.cache.get(client.config.owner.id)?.nickname || client.config.owner.username,
52-
avatarURL: client.config.owner.displayAvatarURL({ format: "png" }),
53-
});
54-
let output = "";
55-
const args = input.split(" ");
56-
const command = args.shift();
57-
const cmd = spawn(`${command}`, args, {
58-
shell: true,
59-
env: { COLUMNS: 128 },
60-
cwd: customCWD || client.config.cwd || process.cwd(),
61-
});
20+
client.customCWD = process.env.CUSTOM_CWD || process.cwd();
6221

63-
cmd.stdout.on("data", (data) => {
64-
output += data;
65-
});
66-
cmd.stderr.on("data", (data) => {
67-
output += data;
68-
});
69-
cmd.on("exit", async () => {
70-
if (output) {
71-
//await client.config.channel.bulkDelete(1);
72-
const chunkStr = (str, n, acc) => {
73-
if (str.length === 0) {
74-
return acc;
75-
} else {
76-
acc.push(str.substring(0, n));
77-
return chunkStr(str.substring(n), n, acc);
78-
}
79-
};
80-
const outputDiscord = chunkStr(output, 4000, []);
22+
logger("info", "Loading events...");
23+
await loadEvents(client);
8124

82-
const embed = new EmbedBuilder().setColor("#4f545c").setTitle("📤 Output").setTimestamp();
83-
let i = 0;
84-
outputDiscord.forEach((item) => {
85-
i++;
86-
embed.setFooter({ text: `Page ${i}/${outputDiscord.length}`, icon: client.user.displayAvatarURL() });
87-
embed.setDescription(`\`\`\`${stripAnsi(item, true) || "No output!"}\`\`\``);
88-
if (i == outputDiscord.length) embed.addFields([{ name: "\u200B", value: `\`\`\`CWD: ${customCWD}\nCPU: ${client.cpuUsage}% | RAM: ${client.memoryPercentage}% | Temp: ${client.cpuTemperature}°C\`\`\`` }]);
89-
const finalMessage = client.config.channel.messages.cache.first();
90-
if (i !== 1) {
91-
client.config.channel.send({ embeds: [embed] });
92-
} else {
93-
finalMessage.reply({ embeds: [embed] });
94-
}
95-
});
96-
}
97-
});
25+
logger("info", "Logging in...");
26+
await client.login(process.env.TOKEN);
27+
} catch (error) {
28+
logger("error", error);
29+
process.exit(1);
9830
}
9931

100-
client.on(Events.MessageCreate, (msg) => {
101-
if (msg.author.bot) return;
102-
if (msg.channel === client.config.channel && msg.author === client.config.owner) {
103-
if (msg.content.startsWith("cd")) {
104-
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");
105-
msg.reply({ embeds: [cd] });
106-
exec(msg.content.split(" ").slice(2).join(" "), null, msg.content.split(" ")[1].toString());
107-
} else {
108-
const wait = new EmbedBuilder().setDescription("<a:loading:895227261752582154> **Waiting for server response...**").setColor("#5865f2");
109-
msg.reply({ embeds: [wait] });
110-
exec(msg.content, null, client.config.cwd);
111-
}
112-
}
113-
});
114-
115-
client.once(Events.ClientReady, async () => {
116-
client.config.channel = client.channels.cache.get(client.config.channel);
117-
if (!client.config.channel) {
118-
throw new Error("Invalid CHANNEL_ID in .env!");
119-
}
120-
client.config.owner = await client.users.fetch(client.config.owner);
121-
if (!client.config.owner) {
122-
throw new Error("Invalid OWNER_ID in .env!");
123-
}
124-
125-
if (!(await client.config.channel.fetchWebhooks()).size) await client.config.channel.createWebhook(client.config.owner.tag, { avatar: client.config.owner.displayAvatarURL({ format: "png" }) });
126-
console.log(chalk.cyan(chalk.bold(`[DISCORD] > Logged in as ${client.user.tag}`)));
127-
client.user.setActivity("all ports!", { type: ActivityType.Watching });
128-
});
129-
130-
process.stdin.on("data", (data) => exec(data.toString(), { terminal: true }));
131-
132-
client.login(client.config.token).catch(() => {
133-
throw new Error("Invalid TOKEN in .env");
134-
});
135-
13632
process.on("unhandledRejection", async (reason) => {
137-
return console.log(chalk.red(chalk.bold(`[ERROR] > Unhandled Rejection: ${reason}`)));
33+
return logger("error", reason);
13834
});
35+
13936
process.on("uncaughtException", async (err) => {
140-
return console.log(chalk.red(chalk.bold(`[ERROR] > Uncaught Exception: ${err}`)));
141-
});
142-
process.on("uncaughtExceptionMonitor", async (err) => {
143-
return console.log(chalk.red(chalk.bold(`[ERROR] > Uncaught Exception Monitor: ${err}`)));
37+
return logger("error", err);
14438
});

0 commit comments

Comments
 (0)