Added helpers files
This commit is contained in:
parent
5723ea79e2
commit
396a8adf72
11 changed files with 929 additions and 0 deletions
80
src/helpers/BotUtils.js
Normal file
80
src/helpers/BotUtils.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
const { getJson } = require("@helpers/HttpUtils");
|
||||
const { success, warn, error } = require("@helpers/Logger");
|
||||
|
||||
module.exports = class BotUtils {
|
||||
/**
|
||||
* Check if the bot is up to date
|
||||
*/
|
||||
static async checkForUpdates() {
|
||||
const response = await getJson("https://toastielab.dev/api/v1/repos/toastie_t0ast/discord-bot/releases/latest");
|
||||
if (!response.success) return error("VersionCheck: Failed to check for bot updates");
|
||||
if (response.data) {
|
||||
if (
|
||||
require("@root/package.json").version.replace(/[^0-9]/g, "") >= response.data.tag_name.replace(/[^0-9]/g, "")
|
||||
) {
|
||||
success("VersionCheck: Your discord bot is up to date");
|
||||
} else {
|
||||
warn(`VersionCheck: ${response.data.tag_name} update is available`);
|
||||
warn("download: https://toastielab.dev/toastie_t0ast/discord-bot/releases/latest");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image url from the message
|
||||
* @param {import('discord.js').Message} message
|
||||
* @param {string[]} args
|
||||
*/
|
||||
static async getImageFromMessage(message, args) {
|
||||
let url;
|
||||
|
||||
// check for attachments
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
const attachUrl = attachment.url;
|
||||
const attachIsImage = attachUrl.endsWith(".png") || attachUrl.endsWith(".jpg") || attachUrl.endsWith(".jpeg");
|
||||
if (attachIsImage) url = attachUrl;
|
||||
}
|
||||
|
||||
if (!url && args.length === 0) url = message.author.displayAvatarURL({ size: 256, extension: "png" });
|
||||
|
||||
if (!url && args.length !== 0) {
|
||||
try {
|
||||
url = new URL(args[0]).href;
|
||||
} catch (ex) {
|
||||
/* Ignore */
|
||||
}
|
||||
}
|
||||
|
||||
if (!url && message.mentions.users.size > 0) {
|
||||
url = message.mentions.users.first().displayAvatarURL({ size: 256, extension: "png" });
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
const member = await message.guild.resolveMember(args[0]);
|
||||
if (member) url = member.user.displayAvatarURL({ size: 256, extension: "png" });
|
||||
}
|
||||
|
||||
if (!url) url = message.author.displayAvatarURL({ size: 256, extension: "png" });
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
static get musicValidations() {
|
||||
return [
|
||||
{
|
||||
callback: ({ client, guildId }) => client.musicManager.getPlayer(guildId),
|
||||
message: "🚫 No music is being played!",
|
||||
},
|
||||
{
|
||||
callback: ({ member }) => member.voice?.channelId,
|
||||
message: "🚫 You need to join my voice channel.",
|
||||
},
|
||||
{
|
||||
callback: ({ member, client, guildId }) =>
|
||||
member.voice?.channelId === client.musicManager.getPlayer(guildId)?.channelId,
|
||||
message: "🚫 You're not in the same voice channel.",
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
107
src/helpers/HttpUtils.js
Normal file
107
src/helpers/HttpUtils.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
const ISO6391 = require("iso-639-1");
|
||||
const sourcebin = require("sourcebin_js");
|
||||
const { error, debug } = require("@helpers/Logger");
|
||||
const fetch = require("node-fetch");
|
||||
const { translate: gTranslate } = require("@vitalets/google-translate-api");
|
||||
|
||||
module.exports = class HttpUtils {
|
||||
/**
|
||||
* Returns JSON response from url
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
*/
|
||||
static async getJson(url, options) {
|
||||
try {
|
||||
// with auth
|
||||
const response = options ? await fetch(url, options) : await fetch(url);
|
||||
const json = await response.json();
|
||||
return {
|
||||
success: response.status === 200 ? true : false,
|
||||
status: response.status,
|
||||
data: json,
|
||||
};
|
||||
} catch (ex) {
|
||||
debug(`Url: ${url}`);
|
||||
error(`getJson`, ex);
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns buffer from url
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
*/
|
||||
static async getBuffer(url, options) {
|
||||
try {
|
||||
const response = options ? await fetch(url, options) : await fetch(url);
|
||||
const buffer = await response.buffer();
|
||||
if (response.status !== 200) debug(response);
|
||||
return {
|
||||
success: response.status === 200 ? true : false,
|
||||
status: response.status,
|
||||
buffer,
|
||||
};
|
||||
} catch (ex) {
|
||||
debug(`Url: ${url}`);
|
||||
error(`getBuffer`, ex);
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the provided content to the provided language code
|
||||
* @param {string} content
|
||||
* @param {string} outputCode
|
||||
*/
|
||||
static async translate(content, outputCode) {
|
||||
try {
|
||||
const { text, raw } = await gTranslate(content, { to: outputCode });
|
||||
return {
|
||||
input: raw.src,
|
||||
output: text,
|
||||
inputCode: raw.src,
|
||||
outputCode,
|
||||
inputLang: ISO6391.getName(raw.src),
|
||||
outputLang: ISO6391.getName(outputCode),
|
||||
};
|
||||
} catch (ex) {
|
||||
error("translate", ex);
|
||||
debug(`Content - ${content} OutputCode: ${outputCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the provided content to the BIN
|
||||
* @param {string} content
|
||||
* @param {string} title
|
||||
*/
|
||||
static async postToBin(content, title) {
|
||||
try {
|
||||
const response = await sourcebin.create(
|
||||
[
|
||||
{
|
||||
name: " ",
|
||||
content,
|
||||
languageId: "text",
|
||||
},
|
||||
],
|
||||
{
|
||||
title,
|
||||
description: " ",
|
||||
}
|
||||
);
|
||||
return {
|
||||
url: response.url,
|
||||
short: response.short,
|
||||
raw: `https://cdn.sourceb.in/bins/${response.key}/0`,
|
||||
};
|
||||
} catch (ex) {
|
||||
error(`postToBin`, ex);
|
||||
}
|
||||
}
|
||||
};
|
94
src/helpers/Logger.js
Normal file
94
src/helpers/Logger.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
const config = require("@root/config");
|
||||
const { EmbedBuilder, WebhookClient } = require("discord.js");
|
||||
const pino = require("pino");
|
||||
|
||||
const webhookLogger = process.env.ERROR_LOGS ? new WebhookClient({ url: process.env.ERROR_LOGS }) : undefined;
|
||||
|
||||
const today = new Date();
|
||||
const pinoLogger = pino.default(
|
||||
{
|
||||
level: "debug",
|
||||
},
|
||||
pino.multistream([
|
||||
{
|
||||
level: "info",
|
||||
stream: pino.transport({
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: "yyyy-mm-dd HH:mm:ss",
|
||||
ignore: "pid,hostname",
|
||||
singleLine: false,
|
||||
hideObject: true,
|
||||
customColors: "info:blue,warn:yellow,error:red",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
level: "debug",
|
||||
stream: pino.destination({
|
||||
dest: `${process.cwd()}/logs/combined-${today.getFullYear()}.${today.getMonth() + 1}.${today.getDate()}.log`,
|
||||
sync: true,
|
||||
mkdir: true,
|
||||
}),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
function sendWebhook(content, err) {
|
||||
if (!content && !err) return;
|
||||
const errString = err?.stack || err;
|
||||
|
||||
const embed = new EmbedBuilder().setColor(config.EMBED_COLORS.ERROR).setAuthor({ name: err?.name || "Error" });
|
||||
|
||||
if (errString)
|
||||
embed.setDescription(
|
||||
"```js\n" + (errString.length > 4096 ? `${errString.substr(0, 4000)}...` : errString) + "\n```"
|
||||
);
|
||||
|
||||
embed.addFields({ name: "Description", value: content || err?.message || "NA" });
|
||||
webhookLogger.send({ username: "Logs", embeds: [embed] }).catch((ex) => { });
|
||||
}
|
||||
|
||||
module.exports = class Logger {
|
||||
/**
|
||||
* @param {string} content
|
||||
*/
|
||||
static success(content) {
|
||||
pinoLogger.info(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
*/
|
||||
static log(content) {
|
||||
pinoLogger.info(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
*/
|
||||
static warn(content) {
|
||||
pinoLogger.warn(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {object} ex
|
||||
*/
|
||||
static error(content, ex) {
|
||||
if (ex) {
|
||||
pinoLogger.error(ex, `${content}: ${ex?.message}`);
|
||||
} else {
|
||||
pinoLogger.error(content);
|
||||
}
|
||||
if (webhookLogger) sendWebhook(content, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
*/
|
||||
static debug(content) {
|
||||
pinoLogger.debug(content);
|
||||
}
|
||||
};
|
5
src/helpers/ModUtil.js
Normal file
5
src/helpers/ModUtil.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const {Collection, EmbedBuilder, GuildMember} = require("discord.js");
|
||||
const { MODERATION } = require("@root/config");
|
||||
|
||||
// Utils
|
||||
const
|
136
src/helpers/Utils.js
Normal file
136
src/helpers/Utils.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
const { COLORS } = require("@src/data.json");
|
||||
const { readdirSync, lstatSync } = require("fs");
|
||||
const { join, extname } = require("path");
|
||||
const permissions = require("./permissions");
|
||||
|
||||
module.exports = class Utils {
|
||||
/**
|
||||
* Checks if a string contains a URL
|
||||
* @param {string} text
|
||||
*/
|
||||
static containsLink(text) {
|
||||
return /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid discord invite
|
||||
* @param {string} text
|
||||
*/
|
||||
static containsDiscordInvite(text) {
|
||||
return /(https?:\/\/)?(www.)?(discord.(gg|io|me|li|link|plus)|discorda?p?p?.com\/invite|invite.gg|dsc.gg|urlcord.cf)\/[^\s/]+?(?=\b)/.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random number below a max
|
||||
* @param {number} max
|
||||
*/
|
||||
static getRandomInt(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid Hex color
|
||||
* @param {string} text
|
||||
*/
|
||||
static isHex(text) {
|
||||
return /^#[0-9A-F]{6}$/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid Hex color
|
||||
* @param {string} text
|
||||
*/
|
||||
static isValidColor(text) {
|
||||
if (COLORS.indexOf(text) > -1) {
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hour difference between two dates
|
||||
* @param {Date} dt2
|
||||
* @param {Date} dt1
|
||||
*/
|
||||
static diffHours(dt2, dt1) {
|
||||
let diff = (dt2.getTime() - dt1.getTime()) / 1000;
|
||||
diff /= 60 * 60;
|
||||
return Math.abs(Math.round(diff));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns remaining time in days, hours, minutes and seconds
|
||||
* @param {number} timeInSeconds
|
||||
*/
|
||||
static timeformat(timeInSeconds) {
|
||||
const days = Math.floor((timeInSeconds % 31536000) / 86400);
|
||||
const hours = Math.floor((timeInSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((timeInSeconds % 3600) / 60);
|
||||
const seconds = Math.round(timeInSeconds % 60);
|
||||
return (
|
||||
(days > 0 ? `${days} days, ` : "") +
|
||||
(hours > 0 ? `${hours} hours, ` : "") +
|
||||
(minutes > 0 ? `${minutes} minutes, ` : "") +
|
||||
(seconds > 0 ? `${seconds} seconds` : "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts duration to milliseconds
|
||||
* @param {string} duration
|
||||
*/
|
||||
static durationToMillis(duration) {
|
||||
return (
|
||||
duration
|
||||
.split(":")
|
||||
.map(Number)
|
||||
.reduce((acc, curr) => curr + acc * 60) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns time remaining until provided date
|
||||
* @param {Date} timeUntil
|
||||
*/
|
||||
static getRemainingTime(timeUntil) {
|
||||
const seconds = Math.abs((timeUntil - new Date()) / 1000);
|
||||
const time = Utils.timeformat(seconds);
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("discord.js").PermissionResolvable[]} perms
|
||||
*/
|
||||
static parsePermissions(perms) {
|
||||
const permissionWord = `permission${perms.length > 1 ? "s" : ""}`;
|
||||
return "`" + perms.map((perm) => permissions[perm]).join(", ") + "` " + permissionWord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively searches for a file in a directory
|
||||
* @param {string} dir
|
||||
* @param {string[]} allowedExtensions
|
||||
*/
|
||||
static recursiveReadDirSync(dir, allowedExtensions = [".js"]) {
|
||||
const filePaths = [];
|
||||
const readCommands = (dir) => {
|
||||
const files = readdirSync(join(process.cwd(), dir));
|
||||
files.forEach((file) => {
|
||||
const stat = lstatSync(join(process.cwd(), dir, file));
|
||||
if (stat.isDirectory()) {
|
||||
readCommands(join(dir, file));
|
||||
} else {
|
||||
const extension = extname(file);
|
||||
if (!allowedExtensions.includes(extension)) return;
|
||||
const filePath = join(process.cwd(), dir, file);
|
||||
filePaths.push(filePath);
|
||||
}
|
||||
});
|
||||
};
|
||||
readCommands(dir);
|
||||
return filePaths;
|
||||
}
|
||||
};
|
227
src/helpers/Validator.js
Normal file
227
src/helpers/Validator.js
Normal file
|
@ -0,0 +1,227 @@
|
|||
const CommandCategory = require("@structures/CommandCategory");
|
||||
const permissions = require("./permissions");
|
||||
const config = require("@root/config");
|
||||
const { log, warn, error } = require("./Logger");
|
||||
const { ApplicationCommandType } = require("discord.js");
|
||||
|
||||
module.exports = class Validator {
|
||||
static validateConfiguration() {
|
||||
log("Validating config file and environment variables");
|
||||
|
||||
// Bot Token
|
||||
if (!process.env.BOT_TOKEN) {
|
||||
error("env: BOT_TOKEN cannot be empty");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Database Config
|
||||
if (!process.env.MONGO_CONNECTION) {
|
||||
error("env: MONGO_CONNECTION cannot be empty");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate Dashboard Config
|
||||
if (config.DASHBOARD.enabled) {
|
||||
if (!process.env.BOT_SECRET) {
|
||||
error("env: BOT_SECRET cannot be empty");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!process.env.SESSION_PASSWORD) {
|
||||
error("env: SESSION_PASSWORD cannot be empty");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.DASHBOARD.baseURL || !config.DASHBOARD.failureURL || !config.DASHBOARD.port) {
|
||||
error("config.js: DASHBOARD details cannot be empty");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache Size
|
||||
if (isNaN(config.CACHE_SIZE.GUILDS) || isNaN(config.CACHE_SIZE.USERS) || isNaN(config.CACHE_SIZE.MEMBERS)) {
|
||||
error("config.js: CACHE_SIZE must be a positive integer");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Music
|
||||
if (config.MUSIC.ENABLED) {
|
||||
if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) {
|
||||
warn("env: SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET are missing. Spotify music links won't work");
|
||||
}
|
||||
if (config.MUSIC.LAVALINK_NODES.length == 0) {
|
||||
warn("config.js: There must be at least one node for Lavalink");
|
||||
}
|
||||
if (!["YT", "YTM", "SC"].includes(config.MUSIC.DEFAULT_SOURCE)) {
|
||||
warn("config.js: MUSIC.DEFAULT_SOURCE must be either YT, YTM or SC");
|
||||
}
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if (config.OWNER_IDS.length === 0) warn("config.js: OWNER_IDS are empty");
|
||||
if (!config.SUPPORT_SERVER) warn("config.js: SUPPORT_SERVER is not provided");
|
||||
if (!process.env.WEATHERSTACK_KEY) warn("env: WEATHERSTACK_KEY is missing. Weather command won't work");
|
||||
if (!process.env.STRANGE_API_KEY) warn("env: STRANGE_API_KEY is missing. Image commands won't work");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@structures/Command')} cmd
|
||||
*/
|
||||
static validateCommand(cmd) {
|
||||
if (typeof cmd !== "object") {
|
||||
throw new TypeError("Command data must be an Object.");
|
||||
}
|
||||
if (typeof cmd.name !== "string" || cmd.name !== cmd.name.toLowerCase()) {
|
||||
throw new Error("Command name must be a lowercase string.");
|
||||
}
|
||||
if (typeof cmd.description !== "string") {
|
||||
throw new TypeError("Command description must be a string.");
|
||||
}
|
||||
if (cmd.cooldown && typeof cmd.cooldown !== "number") {
|
||||
throw new TypeError("Command cooldown must be a number");
|
||||
}
|
||||
if (cmd.category) {
|
||||
if (!Object.prototype.hasOwnProperty.call(CommandCategory, cmd.category)) {
|
||||
throw new Error(`Not a valid category ${cmd.category}`);
|
||||
}
|
||||
}
|
||||
if (cmd.userPermissions) {
|
||||
if (!Array.isArray(cmd.userPermissions)) {
|
||||
throw new TypeError("Command userPermissions must be an Array of permission key strings.");
|
||||
}
|
||||
for (const perm of cmd.userPermissions) {
|
||||
if (!permissions[perm]) throw new RangeError(`Invalid command userPermission: ${perm}`);
|
||||
}
|
||||
}
|
||||
if (cmd.botPermissions) {
|
||||
if (!Array.isArray(cmd.botPermissions)) {
|
||||
throw new TypeError("Command botPermissions must be an Array of permission key strings.");
|
||||
}
|
||||
for (const perm of cmd.botPermissions) {
|
||||
if (!permissions[perm]) throw new RangeError(`Invalid command botPermission: ${perm}`);
|
||||
}
|
||||
}
|
||||
if (cmd.validations) {
|
||||
if (!Array.isArray(cmd.validations)) {
|
||||
throw new TypeError("Command validations must be an Array of validation Objects.");
|
||||
}
|
||||
for (const validation of cmd.validations) {
|
||||
if (typeof validation !== "object") {
|
||||
throw new TypeError("Command validations must be an object.");
|
||||
}
|
||||
if (typeof validation.callback !== "function") {
|
||||
throw new TypeError("Command validation callback must be a function.");
|
||||
}
|
||||
if (typeof validation.message !== "string") {
|
||||
throw new TypeError("Command validation message must be a string.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Command Details
|
||||
if (cmd.command) {
|
||||
if (typeof cmd.command !== "object") {
|
||||
throw new TypeError("Command.command must be an object");
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(cmd.command, "enabled") && typeof cmd.command.enabled !== "boolean") {
|
||||
throw new TypeError("Command.command enabled must be a boolean value");
|
||||
}
|
||||
if (
|
||||
cmd.command.aliases &&
|
||||
(!Array.isArray(cmd.command.aliases) ||
|
||||
cmd.command.aliases.some((ali) => typeof ali !== "string" || ali !== ali.toLowerCase()))
|
||||
) {
|
||||
throw new TypeError("Command.command aliases must be an Array of lowercase strings.");
|
||||
}
|
||||
if (cmd.command.usage && typeof cmd.command.usage !== "string") {
|
||||
throw new TypeError("Command.command usage must be a string");
|
||||
}
|
||||
if (cmd.command.minArgsCount && typeof cmd.command.minArgsCount !== "number") {
|
||||
throw new TypeError("Command.command minArgsCount must be a number");
|
||||
}
|
||||
if (cmd.command.subcommands && !Array.isArray(cmd.command.subcommands)) {
|
||||
throw new TypeError("Command.command subcommands must be an array");
|
||||
}
|
||||
if (cmd.command.subcommands) {
|
||||
for (const sub of cmd.command.subcommands) {
|
||||
if (typeof sub !== "object") {
|
||||
throw new TypeError("Command.command subcommands must be an array of objects");
|
||||
}
|
||||
if (typeof sub.trigger !== "string") {
|
||||
throw new TypeError("Command.command subcommand trigger must be a string");
|
||||
}
|
||||
if (typeof sub.description !== "string") {
|
||||
throw new TypeError("Command.command subcommand description must be a string");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmd.command.enabled && typeof cmd.messageRun !== "function") {
|
||||
throw new TypeError("Missing 'messageRun' function");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Slash Command Details
|
||||
if (cmd.slashCommand) {
|
||||
if (typeof cmd.slashCommand !== "object") {
|
||||
throw new TypeError("Command.slashCommand must be an object");
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(cmd.slashCommand, "enabled") &&
|
||||
typeof cmd.slashCommand.enabled !== "boolean"
|
||||
) {
|
||||
throw new TypeError("Command.slashCommand enabled must be a boolean value");
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(cmd.slashCommand, "ephemeral") &&
|
||||
typeof cmd.slashCommand.ephemeral !== "boolean"
|
||||
) {
|
||||
throw new TypeError("Command.slashCommand ephemeral must be a boolean value");
|
||||
}
|
||||
if (cmd.slashCommand.options && !Array.isArray(cmd.slashCommand.options)) {
|
||||
throw new TypeError("Command.slashCommand options must be a array");
|
||||
}
|
||||
if (cmd.slashCommand.enabled && typeof cmd.interactionRun !== "function") {
|
||||
throw new TypeError("Missing 'interactionRun' function");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@structures/BaseContext')} context
|
||||
*/
|
||||
static validateContext(context) {
|
||||
if (typeof context !== "object") {
|
||||
throw new TypeError("Context must be an object");
|
||||
}
|
||||
if (typeof context.name !== "string" || context.name !== context.name.toLowerCase()) {
|
||||
throw new Error("Context name must be a lowercase string.");
|
||||
}
|
||||
if (typeof context.description !== "string") {
|
||||
throw new TypeError("Context description must be a string.");
|
||||
}
|
||||
if (context.type !== ApplicationCommandType.User && context.type !== ApplicationCommandType.Message) {
|
||||
throw new TypeError("Context type must be a either User/Message.");
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(context, "enabled") && typeof context.enabled !== "boolean") {
|
||||
throw new TypeError("Context enabled must be a boolean value");
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(context, "ephemeral") && typeof context.ephemeral !== "boolean") {
|
||||
throw new TypeError("Context enabled must be a boolean value");
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(context, "defaultPermission") &&
|
||||
typeof context.defaultPermission !== "boolean"
|
||||
) {
|
||||
throw new TypeError("Context defaultPermission must be a boolean value");
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(context, "cooldown") && typeof context.cooldown !== "number") {
|
||||
throw new TypeError("Context cooldown must be a number");
|
||||
}
|
||||
if (context.userPermissions) {
|
||||
if (!Array.isArray(context.userPermissions)) {
|
||||
throw new TypeError("Context userPermissions must be an Array of permission key strings.");
|
||||
}
|
||||
for (const perm of context.userPermissions) {
|
||||
if (!permissions[perm]) throw new RangeError(`Invalid command userPermission: ${perm}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
31
src/helpers/channelTypes.js
Normal file
31
src/helpers/channelTypes.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const { ChannelType } = require("discord.js");
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
*/
|
||||
module.exports = (type) => {
|
||||
switch (type) {
|
||||
case ChannelType.GuildText:
|
||||
return "Guild Text";
|
||||
case ChannelType.GuildVoice:
|
||||
return "Guild Voice";
|
||||
case ChannelType.GuildCategory:
|
||||
return "Guild Category";
|
||||
case ChannelType.GuildAnnouncement:
|
||||
return "Guild Announcement";
|
||||
case ChannelType.AnnouncementThread:
|
||||
return "Guild Announcement Thread";
|
||||
case ChannelType.PublicThread:
|
||||
return "Guild Public Thread";
|
||||
case ChannelType.PrivateThread:
|
||||
return "Guild Private Thread";
|
||||
case ChannelType.GuildStageVoice:
|
||||
return "Guild Stage Voice";
|
||||
case ChannelType.GuildDirectory:
|
||||
return "Guild Directory";
|
||||
case ChannelType.GuildForum:
|
||||
return "Guild Forum";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
151
src/helpers/extenders/Guild.js
Normal file
151
src/helpers/extenders/Guild.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
const { Guild, ChannelType } = require("discord.js");
|
||||
|
||||
const ROLE_MENTION = /<?@?&?(\d{17,20})>?/;
|
||||
const CHANNEL_MENTION = /<?#?(\d{17,20})>?/;
|
||||
const MEMBER_MENTION = /<?@?!?(\d{17,20})>?/;
|
||||
|
||||
/**
|
||||
* Get all channels that match the query
|
||||
* @param {string} query
|
||||
* @param {import("discord.js").GuildChannelTypes[]} type
|
||||
*/
|
||||
Guild.prototype.findMatchingChannels = function (query, type = [ChannelType.GuildText, ChannelType.GuildAnnouncement]) {
|
||||
if (!this || !query || typeof query !== "string") return [];
|
||||
|
||||
const channelManager = this.channels.cache.filter((ch) => type.includes(ch.type));
|
||||
|
||||
const patternMatch = query.match(CHANNEL_MENTION);
|
||||
if (patternMatch) {
|
||||
const id = patternMatch[1];
|
||||
const channel = channelManager.find((r) => r.id === id);
|
||||
if (channel) return [channel];
|
||||
}
|
||||
|
||||
const exact = [];
|
||||
const startsWith = [];
|
||||
const includes = [];
|
||||
channelManager.forEach((ch) => {
|
||||
const lowerName = ch.name.toLowerCase();
|
||||
if (ch.name === query) exact.push(ch);
|
||||
if (lowerName.startsWith(query.toLowerCase())) startsWith.push(ch);
|
||||
if (lowerName.includes(query.toLowerCase())) includes.push(ch);
|
||||
});
|
||||
|
||||
if (exact.length > 0) return exact;
|
||||
if (startsWith.length > 0) return startsWith;
|
||||
if (includes.length > 0) return includes;
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all channels that match the query
|
||||
* @param {string} query
|
||||
* @param {import("discord.js").GuildChannelTypes[]} type
|
||||
*/
|
||||
Guild.prototype.findMatchingVoiceChannels = function (
|
||||
query,
|
||||
type = [ChannelType.GuildVoice, ChannelType.GuildStageVoice]
|
||||
) {
|
||||
if (!this || !query || typeof query !== "string") return [];
|
||||
|
||||
const channelManager = this.channels.cache.filter((ch) => type.includes(ch.type));
|
||||
|
||||
const patternMatch = query.match(CHANNEL_MENTION);
|
||||
if (patternMatch) {
|
||||
const id = patternMatch[1];
|
||||
const channel = channelManager.find((r) => r.id === id);
|
||||
if (channel) return [channel];
|
||||
}
|
||||
|
||||
const exact = [];
|
||||
const startsWith = [];
|
||||
const includes = [];
|
||||
channelManager.forEach((ch) => {
|
||||
const lowerName = ch.name.toLowerCase();
|
||||
if (ch.name === query) exact.push(ch);
|
||||
if (lowerName.startsWith(query.toLowerCase())) startsWith.push(ch);
|
||||
if (lowerName.includes(query.toLowerCase())) includes.push(ch);
|
||||
});
|
||||
|
||||
if (exact.length > 0) return exact;
|
||||
if (startsWith.length > 0) return startsWith;
|
||||
if (includes.length > 0) return includes;
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all roles that match the query
|
||||
* @param {string} query
|
||||
*/
|
||||
Guild.prototype.findMatchingRoles = function (query) {
|
||||
if (!this || !query || typeof query !== "string") return [];
|
||||
|
||||
const patternMatch = query.match(ROLE_MENTION);
|
||||
if (patternMatch) {
|
||||
const id = patternMatch[1];
|
||||
const role = this.roles.cache.find((r) => r.id === id);
|
||||
if (role) return [role];
|
||||
}
|
||||
|
||||
const exact = [];
|
||||
const startsWith = [];
|
||||
const includes = [];
|
||||
this.roles.cache.forEach((role) => {
|
||||
const lowerName = role.name.toLowerCase();
|
||||
if (role.name === query) exact.push(role);
|
||||
if (lowerName.startsWith(query.toLowerCase())) startsWith.push(role);
|
||||
if (lowerName.includes(query.toLowerCase())) includes.push(role);
|
||||
});
|
||||
if (exact.length > 0) return exact;
|
||||
if (startsWith.length > 0) return startsWith;
|
||||
if (includes.length > 0) return includes;
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a guild member from search query
|
||||
* @param {string} query
|
||||
* @param {boolean} exact
|
||||
*/
|
||||
Guild.prototype.resolveMember = async function (query, exact = false) {
|
||||
if (!query || typeof query !== "string") return;
|
||||
|
||||
// Check if mentioned or ID is passed
|
||||
const patternMatch = query.match(MEMBER_MENTION);
|
||||
if (patternMatch) {
|
||||
const id = patternMatch[1];
|
||||
const fetched = await this.members.fetch({ user: id }).catch(() => {});
|
||||
if (fetched) return fetched;
|
||||
}
|
||||
|
||||
// Fetch and cache members from API
|
||||
await this.members.fetch({ query }).catch(() => {});
|
||||
|
||||
// Check if exact tag is matched
|
||||
const matchingTags = this.members.cache.filter((mem) => mem.user.tag === query);
|
||||
if (matchingTags.size === 1) return matchingTags.first();
|
||||
|
||||
// Check for matching username
|
||||
if (!exact) {
|
||||
return this.members.cache.find(
|
||||
(x) =>
|
||||
x.user.username === query ||
|
||||
x.user.username.toLowerCase().includes(query.toLowerCase()) ||
|
||||
x.displayName.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch member stats
|
||||
*/
|
||||
Guild.prototype.fetchMemberStats = async function () {
|
||||
const all = await this.members.fetch({
|
||||
force: false,
|
||||
cache: false,
|
||||
});
|
||||
const total = all.size;
|
||||
const bots = all.filter((mem) => mem.user.bot).size;
|
||||
const members = total - bots;
|
||||
return [total, bots, members];
|
||||
};
|
30
src/helpers/extenders/GuildChannel.js
Normal file
30
src/helpers/extenders/GuildChannel.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const { GuildChannel, ChannelType } = require("discord.js");
|
||||
|
||||
/**
|
||||
* Check if bot has permission to send embeds
|
||||
*/
|
||||
GuildChannel.prototype.canSendEmbeds = function () {
|
||||
return this.permissionsFor(this.guild.members.me).has(["ViewChannel", "SendMessages", "EmbedLinks"]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely send a message to the channel
|
||||
* @param {string|import('discord.js').MessagePayload|import('discord.js').MessageOptions} content
|
||||
* @param {number} [seconds]
|
||||
*/
|
||||
GuildChannel.prototype.safeSend = async function (content, seconds) {
|
||||
if (!content) return;
|
||||
if (!this.type === ChannelType.GuildText && !this.type === ChannelType.DM) return;
|
||||
|
||||
const perms = ["ViewChannel", "SendMessages"];
|
||||
if (content.embeds && content.embeds.length > 0) perms.push("EmbedLinks");
|
||||
if (!this.permissionsFor(this.guild.members.me).has(perms)) return;
|
||||
|
||||
try {
|
||||
if (!seconds) return await this.send(content);
|
||||
const reply = await this.send(content);
|
||||
setTimeout(() => reply.deletable && reply.delete().catch((ex) => {}), seconds * 1000);
|
||||
} catch (ex) {
|
||||
this.client.logger.error(`safeSend`, ex);
|
||||
}
|
||||
};
|
25
src/helpers/extenders/Message.js
Normal file
25
src/helpers/extenders/Message.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const { Message } = require("discord.js");
|
||||
|
||||
/**
|
||||
* @param {string|import('discord.js').MessagePayload|import('discord.js').MessageOptions} content
|
||||
* @param {number} [seconds]
|
||||
*/
|
||||
Message.prototype.safeReply = async function (content, seconds) {
|
||||
if (!content) return;
|
||||
const perms = ["ViewChannel", "SendMessages"];
|
||||
if (content.embeds && content.embeds.length > 0) perms.push("EmbedLinks");
|
||||
if (this.channel.type !== "DM" && !this.channel.permissionsFor(this.guild.members.me).has(perms)) return;
|
||||
|
||||
perms.push("ReadMessageHistory");
|
||||
if (this.channel.type !== "DM" && !this.channel.permissionsFor(this.guild.members.me).has(perms)) {
|
||||
return this.channel.safeSend(content, seconds);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!seconds) return await this.reply(content);
|
||||
const reply = await this.reply(content);
|
||||
setTimeout(() => reply.deletable && reply.delete().catch((ex) => {}), seconds * 1000);
|
||||
} catch (ex) {
|
||||
this.client.logger.error(`safeReply`, ex);
|
||||
}
|
||||
};
|
43
src/helpers/permissions.js
Normal file
43
src/helpers/permissions.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
module.exports = {
|
||||
AddReactions: "Add Reactions",
|
||||
Administrator: "Administrator",
|
||||
AttachFiles: "Attach files",
|
||||
BanMembers: "Ban members",
|
||||
ChangeNickname: "Change nickname",
|
||||
Connect: "Connect",
|
||||
CreateInstantInvite: "Create instant invite",
|
||||
CreatePrivateThreads: "Create private threads",
|
||||
CreatePublicThreads: "Create public threads",
|
||||
DeafenMembers: "Deafen members",
|
||||
EmbedLinks: "Embed links",
|
||||
KickMembers: "Kick members",
|
||||
ManageChannels: "Manage channels",
|
||||
ManageEmojisAndStickers: "Manage emojis and stickers",
|
||||
ManageEvents: "Manage Events",
|
||||
ManageGuild: "Manage server",
|
||||
ManageMessages: "Manage messages",
|
||||
ManageNicknames: "Manage nicknames",
|
||||
ManageRoles: "Manage roles",
|
||||
ManageThreads: "Manage Threads",
|
||||
ManageWebhooks: "Manage webhooks",
|
||||
MentionEveryone: "Mention everyone",
|
||||
ModerateMembers: "Moderate Members",
|
||||
MoveMembers: "Move members",
|
||||
MuteMembers: "Mute members",
|
||||
PrioritySpeaker: "Priority speaker",
|
||||
ReadMessageHistory: "Read message history",
|
||||
RequestToSpeak: "Request to Speak",
|
||||
SendMessages: "Send messages",
|
||||
SendMessagesInThreads: "Send Messages In Threads",
|
||||
SendTTSMessages: "Send TTS messages",
|
||||
Speak: "Speak",
|
||||
Stream: "Video",
|
||||
UseApplicationCommands: "Use Application Commands",
|
||||
UseEmbeddedActivities: "Use Embedded Activities",
|
||||
UseExternalEmojis: "Use External Emojis",
|
||||
UseExternalStickers: "Use External Stickers",
|
||||
UseVAD: "Use voice activity",
|
||||
ViewAuditLog: "View audit log",
|
||||
ViewChannel: "View channel",
|
||||
ViewGuildInsights: "View server insights",
|
||||
};
|
Loading…
Reference in a new issue