@davipccunha/discordjs-helper
Version:
A package that contains some useful functions to complement discord.js library
540 lines (531 loc) • 18.4 kB
JavaScript
// src/utils/utils.ts
import { BaseInteraction, StringSelectMenuInteraction } from "discord.js";
import { readdirSync, statSync } from "fs";
import { join } from "path";
function randInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function recursiveFiles(rootDirectory, allFiles = []) {
try {
const filesInRoot = readdirSync(rootDirectory);
for (const file of filesInRoot) {
const absolute = join(rootDirectory, file);
if (statSync(absolute).isDirectory()) {
recursiveFiles(absolute, allFiles);
} else {
allFiles.push(absolute);
}
}
} catch (err) {
console.error(err);
}
return new Promise((resolve) => {
resolve(allFiles);
});
}
function extendTypes() {
extendArray();
extendDiscordJS();
}
function extendArray() {
Object.defineProperty(Array.prototype, "random", {
value: function() {
return this[Math.floor(Math.random() * this.length)];
},
writable: true,
configurable: true,
enumerable: false
});
Object.defineProperty(Array.prototype, "draw", {
value: function(number = 1) {
const result = [];
if (number >= this.length) {
return this;
}
while (result.length < number) {
let element = this.random();
if (!result.includes(element)) {
result.push(element);
}
}
return result;
},
writable: true,
configurable: true,
enumerable: false
});
}
function extendDiscordJS() {
extendBaseInteraction();
extendStringSelectMenuInteraction();
}
function extendBaseInteraction() {
Object.defineProperty(BaseInteraction.prototype, "replyOrFollowUp", {
value: async function(reply) {
reply = typeof reply === "string" ? { content: reply } : reply;
if (this.replied || this.deferred) {
return await this.followUp(reply);
} else {
return await this.reply(reply);
}
},
writable: true,
configurable: true,
enumerable: false
});
}
function extendStringSelectMenuInteraction() {
Object.defineProperty(StringSelectMenuInteraction.prototype, "clearSelection", {
value: async function() {
await this.message.edit({ components: this.message.components });
},
writable: true,
configurable: true,
enumerable: false
});
}
// src/models/ErrorMessages.ts
var ErrorMessages = {
NoPermission: "You do not have permission to do that."
};
// src/models/ExtendedClient.ts
import { Client, Collection, IntentsBitField } from "discord.js";
// src/utils/builders/MessageEmbedBuilder.ts
import { EmbedBuilder } from "discord.js";
var MessageEmbedBuilder = class _MessageEmbedBuilder {
embed;
static defaultFooter;
static defaultColor = "NotQuiteBlack";
constructor(data) {
data = {
title: data.title,
description: data.description || " ",
color: data.color || _MessageEmbedBuilder.defaultColor,
showTimestamp: data.showTimestamp ?? true,
defaultFooter: data.defaultFooter ?? true
};
this.embed = new EmbedBuilder(data);
this.embed.setColor(data.color);
if (data.title)
this.embed.setTitle(data.title);
if (data.showTimestamp)
this.embed.setTimestamp();
this.embed.setFooter(_MessageEmbedBuilder.defaultFooter);
}
setDefaultColor(color) {
_MessageEmbedBuilder.defaultColor = color;
return this;
}
setDefaultFooter(footer) {
_MessageEmbedBuilder.defaultFooter = footer;
return this;
}
setTitle(title) {
this.embed.setTitle(title);
return this;
}
setDescription(description) {
this.embed.setDescription(description);
return this;
}
addField(field) {
field.inline = !!field.inline;
this.embed.addFields([field]);
return this;
}
setFields(...fields) {
for (const field of fields) {
field.inline = !!field.inline;
field.value = field.value.toString();
}
this.embed.setFields(fields);
return this;
}
setThumbnail(url) {
this.embed.setThumbnail(url);
return this;
}
setImage(url) {
this.embed.setImage(url);
return this;
}
setAuthor(author) {
this.embed.setAuthor(author);
return this;
}
setColor(color) {
this.embed.setColor(color);
return this;
}
setFooter(footer) {
this.embed.setFooter(footer);
return this;
}
setTimestamp(timestamp) {
this.embed.setTimestamp(timestamp);
return this;
}
setURL(url) {
this.embed.setURL(url);
return this;
}
build() {
return this.embed;
}
static from(embed) {
const builder = new _MessageEmbedBuilder(embed);
builder.embed = EmbedBuilder.from(embed);
return builder;
}
};
// src/utils/decorators/RegisterInteraction.ts
import { ApplicationCommandType } from "discord.js";
var commandsInstances = /* @__PURE__ */ new Set();
var buttonsInstances = /* @__PURE__ */ new Set();
var selectMenusInstances = /* @__PURE__ */ new Set();
var modalsInstances = /* @__PURE__ */ new Set();
function RegisterChatInputCommand(name, description, defaultPermission = true) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: name.toLowerCase(), writable: false });
if (!instance.description) Object.defineProperty(instance, "description", { value: description, writable: false });
if (!instance.defaultPermission) Object.defineProperty(instance, "defaultPermission", { value: defaultPermission, writable: false });
Object.defineProperty(instance, "type", { value: ApplicationCommandType.ChatInput, writable: false });
commandsInstances.add(instance);
return clazz;
};
}
function RegisterMessageCommand(name, defaultPermission = true) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: name, writable: false });
if (!instance.defaultPermission) Object.defineProperty(instance, "defaultPermission", { value: defaultPermission, writable: false });
Object.defineProperty(instance, "type", { value: ApplicationCommandType.Message, writable: false });
commandsInstances.add(instance);
return clazz;
};
}
function RegisterUserCommand(name, defaultPermission = true) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: name, writable: false });
if (!instance.defaultPermission) Object.defineProperty(instance, "defaultPermission", { value: defaultPermission, writable: false });
Object.defineProperty(instance, "type", { value: ApplicationCommandType.User, writable: false });
commandsInstances.add(instance);
return clazz;
};
}
function RegisterButton(id) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: id, writable: false });
buttonsInstances.add(instance);
return clazz;
};
}
function RegisterSelectMenu(id) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: id, writable: false });
selectMenusInstances.add(instance);
return clazz;
};
}
function RegisterModal(id) {
return function(clazz) {
const instance = new clazz();
if (!instance.name) Object.defineProperty(instance, "name", { value: id, writable: false });
modalsInstances.add(instance);
return clazz;
};
}
// src/models/ExtendedClient.ts
var ExtendedClient = class extends Client {
_commands = new Collection();
_buttons = new Collection();
_selectMenus = new Collection();
_modals = new Collection();
constructor(token) {
super({
intents: [
IntentsBitField.Flags.AutoModerationConfiguration,
IntentsBitField.Flags.AutoModerationExecution,
IntentsBitField.Flags.DirectMessageReactions,
IntentsBitField.Flags.DirectMessageTyping,
IntentsBitField.Flags.DirectMessages,
IntentsBitField.Flags.GuildEmojisAndStickers,
IntentsBitField.Flags.GuildIntegrations,
IntentsBitField.Flags.GuildInvites,
IntentsBitField.Flags.GuildMembers,
IntentsBitField.Flags.GuildMessageReactions,
IntentsBitField.Flags.GuildMessageTyping,
IntentsBitField.Flags.GuildMessages,
IntentsBitField.Flags.GuildModeration,
IntentsBitField.Flags.GuildPresences,
IntentsBitField.Flags.GuildScheduledEvents,
IntentsBitField.Flags.GuildVoiceStates,
IntentsBitField.Flags.GuildWebhooks,
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.MessageContent
]
});
this.token = token;
}
/**
* Caches the commands to respond to their interactions once they are triggered
* @param commands The commands to register
*
* @note This method is intended for JavaScript users. TypeScript users should use the decorator `@Register...Command` instead
* @see RegisterChatInputCommandInteraction
*/
async registerCommands(...commands) {
for (const command of commands) {
this._commands.set(command.name, command);
}
}
/**
* Caches the buttons to respond to their interactions once they are triggered
* @param buttons The buttons to register
*
* @note This method is intended for JavaScript users. TypeScript users should use the decorator `@RegisterButton` instead
* @see RegisterButtonInteraction
*/
async registerButtons(...buttons) {
for (const button of buttons) {
this._buttons.set(button.name, button);
}
}
/**
* Caches the select menus to respond to their interactions once they are triggered
* @param selectMenus The select menus to register
*
* @note This method is intended for JavaScript users. TypeScript users should use the decorator `@RegisterSelectMenu` instead
* @see RegisterSelectMenuInteraction
*/
async registerSelectMenus(...selectMenus) {
for (const selectMenu of selectMenus) {
this._selectMenus.set(selectMenu.name, selectMenu);
}
}
/**
* Caches the modals to respond to their interactions once they are triggered
* @param modals The modals to register
*
* @note This method is intended for JavaScript users. TypeScript users should use the decorator `@RegisterModal` instead
* @see RegisterModalInteraction
*/
async registerModals(...modals) {
for (const modal of modals) {
this._modals.set(modal.name, modal);
}
}
/**
* Caches the interactions decorated with the @Register... decorators
*
* @note This method is intended for TypeScript users. JavaScript users should use the explicit methods to register the interactions
*/
async registerInteractions() {
for (const command of commandsInstances) {
this._commands.set(command.name, command);
}
for (const button of buttonsInstances) {
this._buttons.set(button.name, button);
}
for (const selectMenu of selectMenusInstances) {
this._selectMenus.set(selectMenu.name, selectMenu);
}
for (const modal of modalsInstances) {
this._modals.set(modal.name, modal);
}
}
/**
* Loads the cached commands to a guild
* @param guild The guild to create the commands in
*/
async createCommands(guild) {
for (const command of this._commands.values()) {
await guild.commands.create(command).catch(console.error);
}
}
/**
* Deletes a command from a guild
* @param commandName The registered name of the command to delete
* @param guild The guild to delete the command from
*/
async unregisterCommand(commandName, guild) {
guild.commands.create({
name: commandName,
description: "Deleted"
}).then((command) => {
command.delete();
console.log(`Command ${commandName} deleted`);
}).catch(console.error);
}
/**
* Creates the registered commands in the specified guilds
* @param guildIDs The IDs of the guilds to create the commands in. If no IDs are provided, the commands will be created in all guilds the bot is in
*/
async loadCommands(...guildIDs) {
this.once("ready", async () => {
if (guildIDs.length === 0) {
this.guilds.cache.forEach(async (guild) => {
await this.createCommands(guild);
});
} else {
for (const guildID of guildIDs) {
const guild = await this.guilds.fetch(guildID);
if (!guild) return;
await this.createCommands(guild);
}
}
});
}
/**
* Deletes a list of commands from the specified guilds
* @param commandsNames The registered names of the commands to delete
* @param guildIDs The IDs of the guilds to delete the commands from. Defaults to all guilds the bot is in
*/
async deleteCommands(commandsNames, guildIDs = []) {
this.once("ready", async () => {
if (guildIDs.length === 0) {
this.guilds.cache.forEach(async (guild) => {
for (const commandName of commandsNames) {
await this.unregisterCommand(commandName, guild);
}
});
} else {
for (const guildID of guildIDs) {
const guild = await this.guilds.fetch(guildID);
if (!guild) return;
for (const commandName of commandsNames) {
await this.unregisterCommand(commandName, guild);
}
}
}
});
}
/**
* Returns the bot as a member of a guild
* @param guildID The ID of the guild
* @returns Member representation of the bot
*/
async asMember(guildID) {
if (!this.user) return null;
const guild = await this.guilds.fetch(guildID).catch(console.error);
if (!guild) return null;
const member = await guild.members.fetch(this.user.id).catch(console.error);
if (!member) return null;
return member;
}
// Logs a message when the client becomes ready and handle interactions (commands, buttons, select menus, modals)
async handleEvents() {
this.once("ready", async () => {
console.log(`Client logged in @ ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
MessageEmbedBuilder.defaultFooter = { text: this.user.username, iconURL: this.user.displayAvatarURL({ forceStatic: false }) };
});
this.on("interactionCreate", async (interaction) => {
if (interaction.isCommand()) {
const command = this._commands.get(interaction.commandName);
if (!command) return;
await command.execute(interaction, this);
} else if (interaction.isButton()) {
const button = this._buttons.get(interaction.customId);
if (!button) return;
await button.execute(interaction, this);
} else if (interaction.isStringSelectMenu()) {
const selectMenu = this._selectMenus.get(interaction.customId);
if (!selectMenu) return;
await selectMenu.execute(interaction, this);
} else if (interaction.isModalSubmit()) {
const modal = this._modals.get(interaction.customId);
if (!modal) return;
await modal.execute(interaction, this);
}
});
}
/**
* Logs the bot, registers the interactions and starts listening for interactions creation
* @param autoRegisterInteractions This should be set to `false` if you are not using TS or not using the decorators to register the interactions
*/
async start(autoRegisterInteractions = true) {
await this.login();
if (autoRegisterInteractions) await this.registerInteractions();
await this.handleEvents();
}
};
// src/utils/decorators/NoReplyInteraction.ts
function NoReplyInteraction() {
return function(constructor) {
const originalExecute = constructor.prototype.execute;
if (typeof originalExecute !== "function") {
throw new Error(`NoReplyInteraction decorator: The class ${constructor.name} does not have an 'execute' method.`);
}
constructor.prototype.execute = async function(...args) {
const result = await originalExecute.apply(this, args);
const interaction = args[0];
if (!interaction || interaction.replied || interaction.deferred) return result;
await interaction.deferReply().catch(console.error);
await interaction.deleteReply().catch(console.error);
return result;
};
};
}
// src/utils/decorators/RequirePermission.ts
function RequireMemberPermission(...permissions) {
return function(constructor) {
const originalExecute = constructor.prototype.execute;
if (typeof originalExecute !== "function") {
throw new Error(`RequirePermission decorator: The class ${constructor.name} does not have an 'execute' method.`);
}
constructor.prototype.execute = async function(...args) {
const interaction = args[0];
if (!interaction?.member?.permissions) return;
const hasPermission = interaction.member.permissions.has(permissions);
if (!hasPermission) {
if (ErrorMessages.NoPermission)
await interaction.replyOrFollowUp({ content: ErrorMessages.NoPermission, ephemeral: true });
return;
}
return originalExecute.apply(this, args);
};
};
}
function RequireChannelPermission(...permissions) {
return function(constructor) {
const originalExecute = constructor.prototype.execute;
if (typeof originalExecute !== "function") {
throw new Error(`RequirePermission decorator: The class ${constructor.name} does not have an 'execute' method.`);
}
constructor.prototype.execute = async function(...args) {
const interaction = args[0];
if (!interaction?.member?.permissions) return;
if (!interaction.channel || interaction.channel.isDMBased()) return;
const hasPermission = interaction.channel.permissionsFor(interaction.member).has(permissions);
if (!hasPermission) {
if (ErrorMessages.NoPermission)
await interaction.replyOrFollowUp({ content: ErrorMessages.NoPermission, ephemeral: true });
return;
}
return originalExecute.apply(this, args);
};
};
}
// src/index.ts
extendTypes();
export {
ErrorMessages,
ExtendedClient,
MessageEmbedBuilder,
NoReplyInteraction,
RegisterButton,
RegisterChatInputCommand,
RegisterMessageCommand,
RegisterModal,
RegisterSelectMenu,
RegisterUserCommand,
RequireChannelPermission,
RequireMemberPermission,
extendTypes,
randInt,
recursiveFiles
};