Added structures.

This commit is contained in:
Toastie 2024-07-05 14:56:44 +12:00
parent 2073e6eb14
commit 716b402e91
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
5 changed files with 556 additions and 0 deletions

View file

@ -0,0 +1,26 @@
/**
* @typedef {Object} ContextData
* @property {string} name - The name of the command (must be lowercase)
* @property {string} description - A short description of the command
* @property {import('discord.js').ApplicationCommandType} type - The type of application command
* @property {boolean} [enabled] - Whether the slash command is enabled or not
* @property {boolean} [ephemeral] - Whether the reply should be ephemeral
* @property {boolean} [defaultPermission] - Whether default permission must be enabled
* @property {import('discord.js').PermissionResolvable[]} [userPermissions] - Permissions required by the user to use the command.
* @property {number} [cooldown] - Command cooldown in seconds
* @property {function(import('discord.js').ContextMenuCommandInteraction)} run - The callback to be executed when the context is invoked
*/
/**
* @type {ContextData} data - The context information
*/
module.exports = {
name: "",
description: "",
type: "",
enabled: "",
ephemeral: "",
options: true,
userPermissions: [],
cooldown: 0,
}

345
src/structures/BotClient.js Normal file
View file

@ -0,0 +1,345 @@
const {
Client,
Collection,
GatewayIntentBits,
Partials,
WebhookClient,
ApplicationCommandType,
} = require("discord.js");
const path = require("path");
const { table } = require("table");
const Logger = require("../helpers/Logger");
const { recursiveReadDirSync } = require("../helpers/Utils");
const { validateCommand, validateContext } = require("../helpers/Validator");
const { schemas } = require("@src/database/mongoose");
const CommandCategory = require("./CommandCategory");
const lavaclient = require("../handlers/lavaclient");
const giveawaysHandler = require("../handlers/giveaway");
const { DiscordTogether } = require("discord-together");
module.exports = class BotClient extends Client {
constructor() {
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildVoiceStates,
],
partials: [Partials.User, Partials.Message, Partials.Reaction],
allowedMentions: {
repliedUser: false,
},
restRequestTimeout: 20000,
});
this.wait = require("util").promisify(setTimeout); // await client.wait(1000) - Wait 1 second
this.config = require("@root/config"); // load the config file
/**
* @type {import('@structures/Command')[]}
*/
this.commands = []; // store actual command
this.commandIndex = new Collection(); // store (alias, arrayIndex) pair
/**
* @type {Collection<string, import('@structures/Command')>}
*/
this.slashCommands = new Collection(); // store slash commands
/**
* @type {Collection<string, import('@structures/BaseContext')>}
*/
this.contextMenus = new Collection(); // store contextMenus
this.counterUpdateQueue = []; // store guildId's that needs counter update
// initialize webhook for sending guild join/leave details
this.joinLeaveWebhook = process.env.JOIN_LEAVE_LOGS
? new WebhookClient({ url: process.env.JOIN_LEAVE_LOGS })
: undefined;
// Music Player
if (this.config.MUSIC.ENABLED) this.musicManager = lavaclient(this);
// Giveaways
if (this.config.GIVEAWAYS.ENABLED) this.giveawaysManager = giveawaysHandler(this);
// Logger
this.logger = Logger;
// Database
this.database = schemas;
// Discord Together
this.discordTogether = new DiscordTogether(this);
}
/**
* Load all events from the specified directory
* @param {string} directory directory containing the event files
*/
loadEvents(directory) {
this.logger.log(`Loading events...`);
let success = 0;
let failed = 0;
const clientEvents = [];
recursiveReadDirSync(directory).forEach((filePath) => {
const file = path.basename(filePath);
try {
const eventName = path.basename(file, ".js");
const event = require(filePath);
this.on(eventName, event.bind(null, this));
clientEvents.push([file, "✓"]);
delete require.cache[require.resolve(filePath)];
success += 1;
} catch (ex) {
failed += 1;
this.logger.error(`loadEvent - ${file}`, ex);
}
});
console.log(
table(clientEvents, {
header: {
alignment: "center",
content: "Client Events",
},
singleLine: true,
columns: [{ width: 25 }, { width: 5, alignment: "center" }],
})
);
this.logger.log(`Loaded ${success + failed} events. Success (${success}) Failed (${failed})`);
}
/**
* Find command matching the invoke
* @param {string} invoke
* @returns {import('@structures/Command')|undefined}
*/
getCommand(invoke) {
const index = this.commandIndex.get(invoke.toLowerCase());
return index !== undefined ? this.commands[index] : undefined;
}
/**
* Register command file in the client
* @param {import("@structures/Command")} cmd
*/
loadCommand(cmd) {
// Check if category is disabled
if (cmd.category && CommandCategory[cmd.category]?.enabled === false) {
this.logger.debug(`Skipping Command ${cmd.name}. Category ${cmd.category} is disabled`);
return;
}
// Prefix Command
if (cmd.command?.enabled) {
const index = this.commands.length;
if (this.commandIndex.has(cmd.name)) {
throw new Error(`Command ${cmd.name} already registered`);
}
if (Array.isArray(cmd.command.aliases)) {
cmd.command.aliases.forEach((alias) => {
if (this.commandIndex.has(alias)) throw new Error(`Alias ${alias} already registered`);
this.commandIndex.set(alias.toLowerCase(), index);
});
}
this.commandIndex.set(cmd.name.toLowerCase(), index);
this.commands.push(cmd);
} else {
this.logger.debug(`Skipping command ${cmd.name}. Disabled!`);
}
// Slash Command
if (cmd.slashCommand?.enabled) {
if (this.slashCommands.has(cmd.name)) throw new Error(`Slash Command ${cmd.name} already registered`);
this.slashCommands.set(cmd.name, cmd);
} else {
this.logger.debug(`Skipping slash command ${cmd.name}. Disabled!`);
}
}
/**
* Load all commands from the specified directory
* @param {string} directory
*/
loadCommands(directory) {
this.logger.log(`Loading commands...`);
const files = recursiveReadDirSync(directory);
for (const file of files) {
try {
const cmd = require(file);
if (typeof cmd !== "object") continue;
validateCommand(cmd);
this.loadCommand(cmd);
} catch (ex) {
this.logger.error(`Failed to load ${file} Reason: ${ex.message}`);
}
}
this.logger.success(`Loaded ${this.commands.length} commands`);
this.logger.success(`Loaded ${this.slashCommands.size} slash commands`);
if (this.slashCommands.size > 100) throw new Error("A maximum of 100 slash commands can be enabled");
}
/**
* Load all contexts from the specified directory
* @param {string} directory
*/
loadContexts(directory) {
this.logger.log(`Loading contexts...`);
const files = recursiveReadDirSync(directory);
for (const file of files) {
try {
const ctx = require(file);
if (typeof ctx !== "object") continue;
validateContext(ctx);
if (!ctx.enabled) return this.logger.debug(`Skipping context ${ctx.name}. Disabled!`);
if (this.contextMenus.has(ctx.name)) throw new Error(`Context already exists with that name`);
this.contextMenus.set(ctx.name, ctx);
} catch (ex) {
this.logger.error(`Failed to load ${file} Reason: ${ex.message}`);
}
}
const userContexts = this.contextMenus.filter((ctx) => ctx.type === ApplicationCommandType.User).size;
const messageContexts = this.contextMenus.filter((ctx) => ctx.type === ApplicationCommandType.Message).size;
if (userContexts > 3) throw new Error("A maximum of 3 USER contexts can be enabled");
if (messageContexts > 3) throw new Error("A maximum of 3 MESSAGE contexts can be enabled");
this.logger.success(`Loaded ${userContexts} USER contexts`);
this.logger.success(`Loaded ${messageContexts} MESSAGE contexts`);
}
/**
* Register slash command on startup
* @param {string} [guildId]
*/
async registerInteractions(guildId) {
const toRegister = [];
// filter slash commands
if (this.config.INTERACTIONS.SLASH) {
this.slashCommands
.map((cmd) => ({
name: cmd.name,
description: cmd.description,
type: ApplicationCommandType.ChatInput,
options: cmd.slashCommand.options,
}))
.forEach((s) => toRegister.push(s));
}
// filter contexts
if (this.config.INTERACTIONS.CONTEXT) {
this.contextMenus
.map((ctx) => ({
name: ctx.name,
type: ctx.type,
}))
.forEach((c) => toRegister.push(c));
}
// Register GLobally
if (!guildId) {
await this.application.commands.set(toRegister);
}
// Register for a specific guild
else if (guildId && typeof guildId === "string") {
const guild = this.guilds.cache.get(guildId);
if (!guild) {
this.logger.error(`Failed to register interactions in guild ${guildId}`, new Error("No matching guild"));
return;
}
await guild.commands.set(toRegister);
}
// Throw an error
else {
throw new Error("Did you provide a valid guildId to register interactions");
}
this.logger.success("Successfully registered interactions");
}
/**
* @param {string} search
* @param {Boolean} exact
*/
async resolveUsers(search, exact = false) {
if (!search || typeof search !== "string") return [];
const users = [];
// check if userId is passed
const patternMatch = search.match(/(\d{17,20})/);
if (patternMatch) {
const id = patternMatch[1];
const fetched = await this.users.fetch(id, { cache: true }).catch(() => {}); // check if mentions contains the ID
if (fetched) {
users.push(fetched);
return users;
}
}
// check if exact tag is matched in cache
const matchingTags = this.users.cache.filter((user) => user.tag === search);
if (exact && matchingTags.size === 1) users.push(matchingTags.first());
else matchingTags.forEach((match) => users.push(match));
// check matching username
if (!exact) {
this.users.cache
.filter(
(x) =>
x.username === search ||
x.username.toLowerCase().includes(search.toLowerCase()) ||
x.tag.toLowerCase().includes(search.toLowerCase())
)
.forEach((user) => users.push(user));
}
return users;
}
/**
* Get bot's invite
*/
getInvite() {
return this.generateInvite({
scopes: ["bot", "applications.commands"],
permissions: [
"AddReactions",
"AttachFiles",
"BanMembers",
"ChangeNickname",
"Connect",
"DeafenMembers",
"EmbedLinks",
"KickMembers",
"ManageChannels",
"ManageGuild",
"ManageMessages",
"ManageNicknames",
"ManageRoles",
"ModerateMembers",
"MoveMembers",
"MuteMembers",
"PrioritySpeaker",
"ReadMessageHistory",
"SendMessages",
"SendMessagesInThreads",
"Speak",
"ViewChannel",
],
});
}
};

75
src/structures/Command.js Normal file
View file

@ -0,0 +1,75 @@
/**
* @typedef {Object} Validation
* @property {function} callback - The condition to validate
* @property {string} message - The message to be displayed if callback condition is not met
*/
/**
* @typedef {Object} SubCommand
* @property {string} trigger - subcommand invoke
* @property {string} description - subcommand description
*/
/**
* @typedef {"ADMIN"|"ANIME"|"AUTOMOD"|"ECONOMY"|"FUN"|"IMAGE"|"INFORMATION"|"INVITE"|"MODERATION"|"ERELA_JS"|"NONE"|"OWNER"|"SOCIAL"|"SUGGESTION"|"TICKET"|"UTILITY"} CommandCategory
*/
/**
* @typedef {Object} InteractionInfo
* @property {boolean} enabled - Whether the slash command is enabled or not
* @property {boolean} ephemeral - Whether the reply should be ephemeral
* @property {import('discord.js').ApplicationCommandOptionData[]} options - command options
*/
/**
* @typedef {Object} CommandInfo
* @property {boolean} enabled - Whether the command is enabled or not
* @property {string[]} [aliases] - Alternative names for the command (all must be lowercase)
* @property {string} [usage=""] - The command usage format string
* @property {number} [minArgsCount=0] - Minimum number of arguments the command takes (default is 0)
* @property {SubCommand[]} [subcommands=[]] - List of subcommands
*/
/**
* @typedef {Object} CommandData
* @property {string} name - The name of the command (must be lowercase)
* @property {string} description - A short description of the command
* @property {number} cooldown - The command cooldown in seconds
* @property {CommandCategory} category - The category this command belongs to
* @property {import('discord.js').PermissionResolvable[]} [botPermissions] - Permissions required by the client to use the command.
* @property {import('discord.js').PermissionResolvable[]} [userPermissions] - Permissions required by the user to use the command
* @property {Validation[]} [validations] - List of validations to be run before the command is executed
* @property {CommandInfo} command - A short description of the command
* @property {InteractionInfo} slashCommand - A short description of the command
* @property {function(import('discord.js').Message, string[], object)} messageRun - The callback to be executed when the command is invoked
* @property {function(import('discord.js').ChatInputCommandInteraction, object)} interactionRun - The callback to be executed when the interaction is invoked
*/
/**
* Placeholder for command data
* @type {CommandData}
*/
module.exports = {
name: "",
description: "",
cooldown: 0,
isPremium: false,
category: "NONE",
botPermissions: [],
userPermissions: [],
validations: [],
command: {
enabled: true,
aliases: [],
usage: "",
minArgsCount: 0,
subcommands: [],
},
slashCommand: {
enabled: true,
ephemeral: false,
options: [],
},
messageRun: (message, args, data) => {},
interactionRun: (interaction, data) => {},
};

View file

@ -0,0 +1,99 @@
const config = require("@root/config");
module.exports = {
ADMIN: {
name: "Admin",
image: "https://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Settings-icon.png",
emoji: "⚙️",
},
AUTOMOD: {
name: "Automod",
enabled: config.AUTOMOD.ENABLED,
image: "https://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Settings-icon.png",
emoji: "🤖",
},
ANIME: {
name: "Anime",
image: "https://wallpaperaccess.com/full/5680679.jpg",
emoji: "🎨",
},
ECONOMY: {
name: "Economy",
enabled: config.ECONOMY.ENABLED,
image: "https://icons.iconarchive.com/icons/custom-icon-design/pretty-office-11/128/coins-icon.png",
emoji: "🪙",
},
FUN: {
name: "Fun",
image: "https://icons.iconarchive.com/icons/flameia/aqua-smiles/128/make-fun-icon.png",
emoji: "😂",
},
GIVEAWAY: {
name: "Giveaway",
enabled: config.GIVEAWAYS.ENABLED,
image: "https://cdn-icons-png.flaticon.com/512/4470/4470928.png",
emoji: "🎉",
},
IMAGE: {
name: "Image",
enabled: config.IMAGE.ENABLED,
image: "https://icons.iconarchive.com/icons/dapino/summer-holiday/128/photo-icon.png",
emoji: "🖼️",
},
INVITE: {
name: "Invite",
enabled: config.INVITE.ENABLED,
image: "https://cdn4.iconfinder.com/data/icons/general-business/150/Invite-512.png",
emoji: "📨",
},
INFORMATION: {
name: "Information",
image: "https://icons.iconarchive.com/icons/graphicloads/100-flat/128/information-icon.png",
emoji: "🪧",
},
MODERATION: {
name: "Moderation",
enabled: config.MODERATION.ENABLED,
image: "https://icons.iconarchive.com/icons/lawyerwordpress/law/128/Gavel-Law-icon.png",
emoji: "🔨",
},
MUSIC: {
name: "Music",
enabled: config.MUSIC.ENABLED,
image: "https://icons.iconarchive.com/icons/wwalczyszyn/iwindows/256/Music-Library-icon.png",
emoji: "🎵",
},
OWNER: {
name: "Owner",
image: "https://www.pinclipart.com/picdir/middle/531-5318253_web-designing-icon-png-clipart.png",
emoji: "🤴",
},
SOCIAL: {
name: "Social",
image: "https://icons.iconarchive.com/icons/dryicons/aesthetica-2/128/community-users-icon.png",
emoji: "🫂",
},
STATS: {
name: "Statistics",
enabled: config.STATS.ENABLED,
image: "https://icons.iconarchive.com/icons/graphicloads/flat-finance/256/dollar-stats-icon.png",
emoji: "📈",
},
SUGGESTION: {
name: "Suggestion",
enabled: config.SUGGESTIONS.ENABLED,
image: "https://cdn-icons-png.flaticon.com/512/1484/1484815.png",
emoji: "📝",
},
TICKET: {
name: "Ticket",
enabled: config.TICKET.ENABLED,
image: "https://icons.iconarchive.com/icons/custom-icon-design/flatastic-2/512/ticket-icon.png",
emoji: "🎫",
},
UTILITY: {
name: "Utility",
image: "https://icons.iconarchive.com/icons/blackvariant/button-ui-system-folders-alt/128/Utilities-icon.png",
emoji: "🛠",
},
};

11
src/structures/index.js Normal file
View file

@ -0,0 +1,11 @@
const BotClient = require("./BotClient");
const Command = require("./Command");
const CommandCategory = require("./CommandCategory");
const BaseContext = require("./BaseContext");
module.exports = {
BaseContext,
BotClient,
Command,
CommandCategory,
};