Skip to content

Commit c1cedd6

Browse files
committed
Added: Command class
1 parent 2336417 commit c1cedd6

7 files changed

Lines changed: 156 additions & 71 deletions

File tree

src/base/classes/command.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
ChatInputCommandInteraction,
3+
MessageFlags,
4+
PermissionFlagsBits,
5+
SlashCommandBuilder,
6+
SlashCommandOptionsOnlyBuilder,
7+
SlashCommandSubcommandsOnlyBuilder,
8+
} from 'discord.js';
9+
import { logger } from '../../libs/logger.js';
10+
11+
type DiscordSlashCommandBuilder =
12+
| SlashCommandBuilder
13+
| SlashCommandOptionsOnlyBuilder
14+
| Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>
15+
| SlashCommandSubcommandsOnlyBuilder;
16+
17+
interface CommandOptions {
18+
info: DiscordSlashCommandBuilder;
19+
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
20+
guildOnly?: boolean;
21+
ownerOnly?: boolean;
22+
requiredPermissions?: (keyof typeof PermissionFlagsBits)[];
23+
cooldown?: number;
24+
}
25+
26+
export class Command {
27+
data: DiscordSlashCommandBuilder;
28+
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
29+
guildOnly: boolean;
30+
ownerOnly: boolean;
31+
requiredPermissions: (keyof typeof PermissionFlagsBits)[];
32+
cooldown: number;
33+
private _cooldowns: Map<string, number>;
34+
35+
constructor({ info, execute, guildOnly = false, ownerOnly = false, requiredPermissions = [], cooldown = 0 }: CommandOptions) {
36+
this.data = info;
37+
this.execute = execute;
38+
this.guildOnly = guildOnly;
39+
this.ownerOnly = ownerOnly;
40+
this.requiredPermissions = requiredPermissions;
41+
this.cooldown = cooldown;
42+
this._cooldowns = new Map();
43+
}
44+
45+
get name(): string {
46+
return this.data.name;
47+
}
48+
49+
async run(interaction: ChatInputCommandInteraction, ownerId?: string): Promise<void> {
50+
if (this.guildOnly && !interaction.guild) {
51+
await interaction.reply({ content: 'This command is limited to servers', flags: MessageFlags.Ephemeral });
52+
return;
53+
}
54+
55+
if (this.ownerOnly && interaction.user.id !== ownerId) {
56+
await interaction.reply({ content: 'Only the owner can run this command', flags: MessageFlags.Ephemeral });
57+
return;
58+
}
59+
60+
if (this.requiredPermissions.length && interaction.guild) {
61+
const missing = this.requiredPermissions.filter(
62+
p => !interaction.memberPermissions?.has(PermissionFlagsBits[p])
63+
);
64+
if (missing.length) {
65+
await interaction.reply({
66+
content: `You're lacking of the necessary permissions: \`${missing.join(', ')}\``,
67+
flags: MessageFlags.Ephemeral,
68+
});
69+
return;
70+
}
71+
}
72+
73+
if (this.cooldown > 0) {
74+
const now = Date.now();
75+
const expiry = this._cooldowns.get(interaction.user.id);
76+
if (expiry && now < expiry) {
77+
const unixExpiry = Math.floor(expiry / 1000);
78+
await interaction.reply({
79+
content: `You can use \`/${this.name}\` again, please wait <t:${unixExpiry}:R>.`,
80+
flags: MessageFlags.Ephemeral,
81+
});
82+
return;
83+
}
84+
this._cooldowns.set(interaction.user.id, now + this.cooldown * 1000);
85+
setTimeout(() => this._cooldowns.delete(interaction.user.id), this.cooldown * 1000);
86+
}
87+
88+
try {
89+
await this.execute(interaction);
90+
} catch (err) {
91+
logger.fatal(`[Command: ${this.name}]`, err);
92+
const msg = { content: 'Something went wrong.', ephemeral: true };
93+
if (interaction.replied || interaction.deferred) {
94+
await interaction.followUp(msg);
95+
} else {
96+
await interaction.reply(msg);
97+
}
98+
}
99+
}
100+
}

src/commands/booster.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
TextDisplayBuilder,
88
MessageFlags,
99
} from "discord.js";
10-
import { Command } from "../libs/loadCommands.js";
1110
import {
1211
getBooster,
1312
addBoostCount,
@@ -17,10 +16,11 @@ import {
1716
getTotalBoosts,
1817
registerBoost,
1918
} from "../services/boosterService.js";
19+
import { Command } from "../base/classes/command.js";
2020

21-
const boosterCommand: Command = {
22-
data: new SlashCommandBuilder()
23-
.setName("booster")
21+
export default new Command({
22+
info: new SlashCommandBuilder()
23+
.setName("booster")
2424
.setDescription("Booster management commands")
2525
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
2626
.addSubcommand((sub) =>
@@ -64,8 +64,7 @@ const boosterCommand: Command = {
6464
.addSubcommand((sub) =>
6565
sub.setName("stats").setDescription("View server boost statistics")
6666
),
67-
68-
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
67+
async execute(interaction) {
6968
const sub = interaction.options.getSubcommand();
7069

7170
const discordGuild = interaction.guild;
@@ -221,6 +220,4 @@ const boosterCommand: Command = {
221220
return;
222221
}
223222
},
224-
};
225-
226-
export default boosterCommand;
223+
})

src/commands/example_all.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import {
66
ButtonBuilder,
77
ButtonStyle,
88
} from "discord.js";
9-
import { Command } from "../libs/loadCommands.js";
9+
import { Command } from "../base/classes/command.js";
1010

11-
const exampleAllCommand: Command = {
12-
data: new SlashCommandBuilder()
11+
export default new Command({
12+
info: new SlashCommandBuilder()
1313
.setName("example_all")
1414
.setDescription("[TEMP] Preview all bot messages and embeds"),
15-
16-
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
15+
async execute(interaction) {
1716
await interaction.deferReply();
1817

1918
// 1. Boost start greet embed
@@ -103,6 +102,4 @@ const exampleAllCommand: Command = {
103102
"**[6/6] DM sent to booster on boost start:**\n> Thank you for boosting the server! You now have access to booster perks.",
104103
});
105104
},
106-
};
107-
108-
export default exampleAllCommand;
105+
})

src/commands/info.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,48 @@ import {
66
ButtonStyle,
77
ActionRowBuilder,
88
} from "discord.js";
9-
import { Command } from "../libs/loadCommands.js";
109
import axios from "axios";
10+
import { Command } from "../base/classes/command.js";
1111

1212
interface GithubRes {
1313
login: string,
1414
html_url: string
1515
}
16-
const reloadCommand: Command = {
17-
data: new SlashCommandBuilder()
16+
17+
export default new Command({
18+
info: new SlashCommandBuilder()
1819
.setName("bot-info")
1920
.setDescription("Shows information regarding the bot"),
20-
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
21+
async execute(interaction) {
2122
await interaction.deferReply();
2223

2324
const info = await axios.get<GithubRes[]>(
2425
"https://api.github.com/repos/teamboostify/boostify/contributors"
2526
);
2627

27-
const embed = new EmbedBuilder()
28-
.setColor(16712630)
29-
.setThumbnail(interaction.client.user.displayAvatarURL({ size: 2048 }))
30-
.setTitle("Bot information")
31-
.setDescription(
32-
"Boostify is a Discord bot designed to help you manage your server boosts."
33-
)
34-
.addFields(
35-
{
36-
name: "Developers",
37-
value: info.data
38-
.map((user) => `[${user.login}](${user.html_url})`)
39-
.join("\n"),
40-
inline: true,
41-
},
42-
{
43-
name: "How was I made?",
44-
value: "I was built using TypeScript and discord.js.",
45-
inline: false,
46-
}
47-
)
48-
.setTimestamp();
49-
28+
const embed = new EmbedBuilder()
29+
.setColor(16712630)
30+
.setThumbnail(interaction.client.user.displayAvatarURL({ size: 2048 }))
31+
.setTitle("Bot information")
32+
.setDescription(
33+
"Boostify is a Discord bot designed to help you manage your server boosts."
34+
)
35+
.addFields(
36+
{
37+
name: "Developers",
38+
value: info.data
39+
.map((user) => `[${user.login}](${user.html_url})`)
40+
.join("\n"),
41+
inline: true,
42+
},
43+
{
44+
name: "How was I made?",
45+
value: "I was built using TypeScript and discord.js.",
46+
inline: false,
47+
}
48+
)
49+
.setTimestamp();
50+
5051
const website = new ButtonBuilder()
5152
.setStyle(ButtonStyle.Link)
5253
.setLabel("Our Website")
@@ -73,6 +74,5 @@ const embed = new EmbedBuilder()
7374
components: [actionBar],
7475
});
7576
},
76-
};
77+
})
7778

78-
export default reloadCommand;

src/commands/reload.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
import "../libs/loadVariables.js";
22
import {
33
SlashCommandBuilder,
4-
ChatInputCommandInteraction,
54
PermissionFlagsBits,
6-
Client,
7-
Collection,
85
MessageFlags,
96
} from "discord.js";
10-
import { Command } from "../libs/loadCommands.js";
117
import { loadCommands } from "../libs/loadCommands.js";
8+
import { Command } from "../base/classes/command.js";
9+
import { client } from "../index.js";
1210

13-
const reloadCommand: Command = {
14-
data: new SlashCommandBuilder()
11+
export default new Command({
12+
info: new SlashCommandBuilder()
1513
.setName("reload")
1614
.setDescription("Reload all slash commands")
1715
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
18-
19-
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
16+
async execute(interaction) {
2017
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
2118

2219
try {
23-
const client = interaction.client as Client & { commands?: Collection<string, Command> };
24-
2520
await loadCommands(client, process.env.CLIENT_ID, process.env.BOT_TOKEN, process.env.GUILD_ID);
2621

2722
await interaction.editReply("Commands reloaded successfully.");
@@ -30,6 +25,4 @@ const reloadCommand: Command = {
3025
await interaction.editReply("Failed to reload commands.");
3126
}
3227
},
33-
};
34-
35-
export default reloadCommand;
28+
})

src/commands/role.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
TextDisplayBuilder,
77
MessageFlags,
88
} from "discord.js";
9-
import { Command } from "../libs/loadCommands.js";
109
import { ensureBoosterWhileBoosting, getBooster } from "../services/boosterService.js";
1110
import {
1211
createCustomRole,
1312
updateCustomRole,
1413
deleteCustomRole,
1514
} from "../services/roleService.js";
15+
import { Command } from "../base/classes/command.js";
1616

1717
const ACCENT = 0xe642a4;
1818

@@ -26,8 +26,8 @@ function componentsV2(lines: string[]) {
2626
};
2727
}
2828

29-
const roleCommand: Command = {
30-
data: new SlashCommandBuilder()
29+
export default new Command({
30+
info: new SlashCommandBuilder()
3131
.setName("role")
3232
.setDescription("Manage your custom booster role")
3333
.addSubcommand((sub) =>
@@ -55,8 +55,7 @@ const roleCommand: Command = {
5555
.addSubcommand((sub) =>
5656
sub.setName("delete").setDescription("Delete your custom role")
5757
),
58-
59-
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
58+
async execute(interaction) {
6059
const guild = interaction.guild;
6160
if (!guild) {
6261
await interaction.reply(componentsV2(["This command can only be used in a server."]));
@@ -177,6 +176,4 @@ const roleCommand: Command = {
177176
return;
178177
}
179178
},
180-
};
181-
182-
export default roleCommand;
179+
})

src/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import path from "path";
66
import fs from "fs";
77
import { fileURLToPath, pathToFileURL } from "url";
88
import { logger } from "./libs/logger.js";
9+
import chalk from "chalk";
910

1011
const __dirname = fileURLToPath(new URL(".", import.meta.url));
1112

@@ -50,7 +51,7 @@ process.on("unhandledRejection", (reason) => {
5051
);
5152
});
5253

53-
const client = new Client({
54+
export const client = new Client({
5455
intents: [
5556
GatewayIntentBits.Guilds,
5657
GatewayIntentBits.GuildMembers,
@@ -71,9 +72,9 @@ for (const file of commandFiles) {
7172
const command = (await import(pathToFileURL(filePath).href)).default;
7273
if (command != undefined && Object.keys(command).length !== 0) {
7374
client.commands.set(command.data.name, command);
74-
logger.startup(`Loaded command ${file.replace(/\.[jt]s$/, "")}`);
75+
logger.startup(`Loaded command ${chalk.bold(file.replace(/\.[jt]s$/, ''))}`);
7576
} else {
76-
logger.warn(`Couldn't load command ${file.replace(/\.[jt]s$/, "")}`);
77+
logger.warn(`Couldn't load command ${chalk.bold(file.replace(/\.[jt]s$/, ''))}`);
7778
}
7879
}
7980

@@ -91,7 +92,7 @@ for (const file of eventFiles) {
9192
} else {
9293
client.on(name, (...args) => execute(client, ...args));
9394
}
94-
logger.startup(`Loaded event ${file.replace(/\.[jt]s$/, "")}`);
95+
logger.startup(`Loaded event ${file.replace(/\.[jt]s$/, '')}`);
9596
}
9697

9798
await loadCommands(

0 commit comments

Comments
 (0)