Added structures.
This commit is contained in:
parent
2073e6eb14
commit
716b402e91
5 changed files with 556 additions and 0 deletions
26
src/structures/BaseContext.js
Normal file
26
src/structures/BaseContext.js
Normal 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
345
src/structures/BotClient.js
Normal 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
75
src/structures/Command.js
Normal 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) => {},
|
||||
};
|
99
src/structures/CommandCategory.js
Normal file
99
src/structures/CommandCategory.js
Normal 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
11
src/structures/index.js
Normal 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,
|
||||
};
|
Loading…
Reference in a new issue