Added helpers files

This commit is contained in:
Toastie 2024-07-04 23:59:39 +12:00
parent 5723ea79e2
commit 396a8adf72
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
11 changed files with 929 additions and 0 deletions

80
src/helpers/BotUtils.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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}`);
}
}
}
};

View 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";
}
};

View 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];
};

View 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);
}
};

View 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);
}
};

View 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",
};