Added handlers and contexts
This commit is contained in:
parent
716b402e91
commit
8c4017a59b
16 changed files with 2014 additions and 0 deletions
45
src/contexts/avatar.js
Normal file
45
src/contexts/avatar.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
const { EmbedBuilder, ApplicationCommandType } = require("discord.js");
|
||||||
|
const { EMBED_COLORS } = require("@root/config");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('@structures/BaseContext')}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
name: "avatar",
|
||||||
|
description: "displays avatar information about the user",
|
||||||
|
type: ApplicationCommandType.User,
|
||||||
|
enabled: true,
|
||||||
|
ephemeral: true,
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
|
const user = await interaction.client.users.fetch(interaction.targetId);
|
||||||
|
const response = getAvatar(user);
|
||||||
|
await interaction.followUp(response);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAvatar(user) {
|
||||||
|
const x64 = user.displayAvatarURL({ extension: "png", size: 64 });
|
||||||
|
const x128 = user.displayAvatarURL({ extension: "png", size: 128 });
|
||||||
|
const x256 = user.displayAvatarURL({ extension: "png", size: 256 });
|
||||||
|
const x512 = user.displayAvatarURL({ extension: "png", size: 512 });
|
||||||
|
const x1024 = user.displayAvatarURL({ extension: "png", size: 1024 });
|
||||||
|
const x2048 = user.displayAvatarURL({ extension: "png", size: 2048 });
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(`Avatar of ${user.username}`)
|
||||||
|
.setColor(EMBED_COLORS.BOT_EMBED)
|
||||||
|
.setImage(x256)
|
||||||
|
.setDescription(
|
||||||
|
`Links: • [x64](${x64}) ` +
|
||||||
|
`• [x128](${x128}) ` +
|
||||||
|
`• [x256](${x256}) ` +
|
||||||
|
`• [x512](${x512}) ` +
|
||||||
|
`• [x1024](${x1024}) ` +
|
||||||
|
`• [x2048](${x2048}) `
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
embeds: [embed],
|
||||||
|
};
|
||||||
|
}
|
219
src/handlers/automod.js
Normal file
219
src/handlers/automod.js
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
|
const { containsLink, containsDiscordInvite } = require("@helpers/Utils");
|
||||||
|
const { getMember } = require("@schemas/Member");
|
||||||
|
const { addModAction } = require("@helpers/ModUtils");
|
||||||
|
const { AUTOMOD } = require("@root/config");
|
||||||
|
const { addAutoModLogToDb } = require("@schemas/AutomodLogs");
|
||||||
|
|
||||||
|
const antispamCache = new Map();
|
||||||
|
const MESSAGE_SPAM_THRESHOLD = 3000;
|
||||||
|
|
||||||
|
// Cleanup the cache
|
||||||
|
setInterval(
|
||||||
|
() => {
|
||||||
|
antispamCache.forEach((value, key) => {
|
||||||
|
if (Date.now() - value.timestamp > MESSAGE_SPAM_THRESHOLD) {
|
||||||
|
antispamCache.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
10 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the message needs to be moderated and has required permissions
|
||||||
|
* @param {import('discord.js').Message} message
|
||||||
|
*/
|
||||||
|
const shouldModerate = (message) => {
|
||||||
|
const { member, guild, channel } = message;
|
||||||
|
|
||||||
|
// Ignore if bot cannot delete channel messages
|
||||||
|
if (!channel.permissionsFor(guild.members.me)?.has("ManageMessages")) return false;
|
||||||
|
|
||||||
|
// Ignore Possible Guild Moderators
|
||||||
|
if (member.permissions.has(["KickMembers", "BanMembers", "ManageGuild"])) return false;
|
||||||
|
|
||||||
|
// Ignore Possible Channel Moderators
|
||||||
|
if (channel.permissionsFor(message.member).has("ManageMessages")) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform moderation on the message
|
||||||
|
* @param {import('discord.js').Message} message
|
||||||
|
* @param {object} settings
|
||||||
|
*/
|
||||||
|
async function performAutomod(message, settings) {
|
||||||
|
const { automod } = settings;
|
||||||
|
|
||||||
|
if (automod.wh_channels.includes(message.channelId)) return;
|
||||||
|
if (!automod.debug && !shouldModerate(message)) return;
|
||||||
|
|
||||||
|
const { channel, member, guild, content, author, mentions } = message;
|
||||||
|
const logChannel = settings.modlog_channel ? channel.guild.channels.cache.get(settings.modlog_channel) : null;
|
||||||
|
|
||||||
|
let shouldDelete = false;
|
||||||
|
let strikesTotal = 0;
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
// Max mentions
|
||||||
|
if (mentions.members.size > automod.max_mentions) {
|
||||||
|
fields.push({ name: "Mentions", value: `${mentions.members.size}/${automod.max_mentions}`, inline: true });
|
||||||
|
// strikesTotal += mentions.members.size - automod.max_mentions;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maxrole mentions
|
||||||
|
if (mentions.roles.size > automod.max_role_mentions) {
|
||||||
|
fields.push({ name: "RoleMentions", value: `${mentions.roles.size}/${automod.max_role_mentions}`, inline: true });
|
||||||
|
// strikesTotal += mentions.roles.size - automod.max_role_mentions;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (automod.anti_massmention > 0) {
|
||||||
|
// check everyone mention
|
||||||
|
if (mentions.everyone) {
|
||||||
|
fields.push({ name: "Everyone Mention", value: "✓", inline: true });
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check user/role mentions
|
||||||
|
if (mentions.users.size + mentions.roles.size > automod.anti_massmention) {
|
||||||
|
fields.push({
|
||||||
|
name: "User/Role Mentions",
|
||||||
|
value: `${mentions.users.size + mentions.roles.size}/${automod.anti_massmention}`,
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
// strikesTotal += mentions.users.size + mentions.roles.size - automod.anti_massmention;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max Lines
|
||||||
|
if (automod.max_lines > 0) {
|
||||||
|
const count = content.split("\n").length;
|
||||||
|
if (count > automod.max_lines) {
|
||||||
|
fields.push({ name: "New Lines", value: `${count}/${automod.max_lines}`, inline: true });
|
||||||
|
shouldDelete = true;
|
||||||
|
// strikesTotal += Math.ceil((count - automod.max_lines) / automod.max_lines);
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti Attachments
|
||||||
|
if (automod.anti_attachments) {
|
||||||
|
if (message.attachments.size > 0) {
|
||||||
|
fields.push({ name: "Attachments Found", value: "✓", inline: true });
|
||||||
|
shouldDelete = true;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti links
|
||||||
|
if (automod.anti_links) {
|
||||||
|
if (containsLink(content)) {
|
||||||
|
fields.push({ name: "Links Found", value: "✓", inline: true });
|
||||||
|
shouldDelete = true;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti Spam
|
||||||
|
if (!automod.anti_links && automod.anti_spam) {
|
||||||
|
if (containsLink(content)) {
|
||||||
|
const key = author.id + "|" + message.guildId;
|
||||||
|
if (antispamCache.has(key)) {
|
||||||
|
let antispamInfo = antispamCache.get(key);
|
||||||
|
if (
|
||||||
|
antispamInfo.channelId !== message.channelId &&
|
||||||
|
antispamInfo.content === content &&
|
||||||
|
Date.now() - antispamInfo.timestamp < MESSAGE_SPAM_THRESHOLD
|
||||||
|
) {
|
||||||
|
fields.push({ name: "AntiSpam Detection", value: "✓", inline: true });
|
||||||
|
shouldDelete = true;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let antispamInfo = {
|
||||||
|
channelId: message.channelId,
|
||||||
|
content,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
antispamCache.set(key, antispamInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anti Invites
|
||||||
|
if (!automod.anti_links && automod.anti_invites) {
|
||||||
|
if (containsDiscordInvite(content)) {
|
||||||
|
fields.push({ name: "Discord Invites", value: "✓", inline: true });
|
||||||
|
shouldDelete = true;
|
||||||
|
strikesTotal += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete message if deletable
|
||||||
|
if (shouldDelete && message.deletable) {
|
||||||
|
message
|
||||||
|
.delete()
|
||||||
|
.then(() => channel.safeSend("> Auto-Moderation! Message deleted", 5))
|
||||||
|
.catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strikesTotal > 0) {
|
||||||
|
// add strikes to member
|
||||||
|
const memberDb = await getMember(guild.id, author.id);
|
||||||
|
memberDb.strikes += strikesTotal;
|
||||||
|
|
||||||
|
// log to db
|
||||||
|
const reason = fields.map((field) => field.name + ": " + field.value).join("\n");
|
||||||
|
addAutoModLogToDb(member, content, reason, strikesTotal).catch(() => { });
|
||||||
|
|
||||||
|
// send automod log
|
||||||
|
if (logChannel) {
|
||||||
|
const logEmbed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: "Auto Moderation" })
|
||||||
|
.setThumbnail(author.displayAvatarURL())
|
||||||
|
.setColor(AUTOMOD.LOG_EMBED)
|
||||||
|
.addFields(fields)
|
||||||
|
.setDescription(`**Channel:** ${channel.toString()}\n**Content:**\n${content}`)
|
||||||
|
.setFooter({
|
||||||
|
text: `By ${author.username} | ${author.id}`,
|
||||||
|
iconURL: author.avatarURL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logChannel.safeSend({ embeds: [logEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// DM strike details
|
||||||
|
const strikeEmbed = new EmbedBuilder()
|
||||||
|
.setColor(AUTOMOD.DM_EMBED)
|
||||||
|
.setThumbnail(guild.iconURL())
|
||||||
|
.setAuthor({ name: "Auto Moderation" })
|
||||||
|
.addFields(fields)
|
||||||
|
.setDescription(
|
||||||
|
`You have received ${strikesTotal} strikes!\n\n` +
|
||||||
|
`**Guild:** ${guild.name}\n` +
|
||||||
|
`**Total Strikes:** ${memberDb.strikes} out of ${automod.strikes}`
|
||||||
|
);
|
||||||
|
|
||||||
|
author.send({ embeds: [strikeEmbed] }).catch((ex) => { });
|
||||||
|
|
||||||
|
// check if max strikes are received
|
||||||
|
if (memberDb.strikes >= automod.strikes) {
|
||||||
|
// Reset Strikes
|
||||||
|
memberDb.strikes = 0;
|
||||||
|
|
||||||
|
// Add Moderation Action
|
||||||
|
await addModAction(guild.members.me, member, "Automod: Max strikes received", automod.action).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
await memberDb.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
performAutomod,
|
||||||
|
};
|
222
src/handlers/command.js
Normal file
222
src/handlers/command.js
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
|
||||||
|
const { OWNER_IDS, PREFIX_COMMANDS, EMBED_COLORS } = require("@root/config");
|
||||||
|
const { parsePermissions } = require("@helpers/Utils");
|
||||||
|
const { timeformat } = require("@helpers/Utils");
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
|
||||||
|
const cooldownCache = new Map();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Message} message
|
||||||
|
* @param {import("@structures/Command")} cmd
|
||||||
|
* @param {object} settings
|
||||||
|
*/
|
||||||
|
handlePrefixCommand: async function (message, cmd, settings) {
|
||||||
|
const prefix = settings.prefix;
|
||||||
|
const args = message.content.replace(prefix, "").split(/\s+/);
|
||||||
|
const invoke = args.shift().toLowerCase();
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
data.settings = settings;
|
||||||
|
data.prefix = prefix;
|
||||||
|
data.invoke = invoke;
|
||||||
|
|
||||||
|
if (!message.channel.permissionsFor(message.guild.members.me).has("SendMessages")) return;
|
||||||
|
|
||||||
|
// callback validations
|
||||||
|
if (cmd.validations) {
|
||||||
|
for (const validation of cmd.validations) {
|
||||||
|
if (!validation.callback(message)) {
|
||||||
|
return message.safeReply(validation.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner commands
|
||||||
|
if (cmd.category === "OWNER" && !OWNER_IDS.includes(message.author.id)) {
|
||||||
|
return message.safeReply("This command is only accessible to bot owners");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check user permissions
|
||||||
|
if (cmd.userPermissions && cmd.userPermissions?.length > 0) {
|
||||||
|
if (!message.channel.permissionsFor(message.member).has(cmd.userPermissions)) {
|
||||||
|
return message.safeReply(`You need ${parsePermissions(cmd.userPermissions)} for this command`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check bot permissions
|
||||||
|
if (cmd.botPermissions && cmd.botPermissions.length > 0) {
|
||||||
|
if (!message.channel.permissionsFor(message.guild.members.me).has(cmd.botPermissions)) {
|
||||||
|
return message.safeReply(`I need ${parsePermissions(cmd.botPermissions)} for this command`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// minArgs count
|
||||||
|
if (cmd.command.minArgsCount > args.length) {
|
||||||
|
const usageEmbed = this.getCommandUsage(cmd, prefix, invoke);
|
||||||
|
return message.safeReply({ embeds: [usageEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// cooldown check
|
||||||
|
if (cmd.cooldown > 0) {
|
||||||
|
const remaining = getRemainingCooldown(message.author.id, cmd);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return message.safeReply(`You are on cooldown. You can again use the command in \`${timeformat(remaining)}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cmd.messageRun(message, args, data);
|
||||||
|
} catch (ex) {
|
||||||
|
message.client.logger.error("messageRun", ex);
|
||||||
|
message.safeReply("An error occurred while running this command");
|
||||||
|
} finally {
|
||||||
|
if (cmd.cooldown > 0) applyCooldown(message.author.id, cmd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ChatInputCommandInteraction} interaction
|
||||||
|
*/
|
||||||
|
handleSlashCommand: async function (interaction) {
|
||||||
|
const cmd = interaction.client.slashCommands.get(interaction.commandName);
|
||||||
|
if (!cmd) return interaction.reply({ content: "An error has occurred", ephemeral: true }).catch(() => { });
|
||||||
|
|
||||||
|
// callback validations
|
||||||
|
if (cmd.validations) {
|
||||||
|
for (const validation of cmd.validations) {
|
||||||
|
if (!validation.callback(interaction)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: validation.message,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner commands
|
||||||
|
if (cmd.category === "OWNER" && !OWNER_IDS.includes(interaction.user.id)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `This command is only accessible to bot owners`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// user permissions
|
||||||
|
if (interaction.member && cmd.userPermissions?.length > 0) {
|
||||||
|
if (!interaction.member.permissions.has(cmd.userPermissions)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `You need ${parsePermissions(cmd.userPermissions)} for this command`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bot permissions
|
||||||
|
if (cmd.botPermissions && cmd.botPermissions.length > 0) {
|
||||||
|
if (!interaction.guild.members.me.permissions.has(cmd.botPermissions)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `I need ${parsePermissions(cmd.botPermissions)} for this command`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cooldown check
|
||||||
|
if (cmd.cooldown > 0) {
|
||||||
|
const remaining = getRemainingCooldown(interaction.user.id, cmd);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `You are on cooldown. You can again use the command in \`${timeformat(remaining)}\``,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interaction.deferReply({ ephemeral: cmd.slashCommand.ephemeral });
|
||||||
|
const settings = await getSettings(interaction.guild);
|
||||||
|
await cmd.interactionRun(interaction, { settings });
|
||||||
|
} catch (ex) {
|
||||||
|
await interaction.followUp("Oops! An error occurred while running the command");
|
||||||
|
interaction.client.logger.error("interactionRun", ex);
|
||||||
|
} finally {
|
||||||
|
if (cmd.cooldown > 0) applyCooldown(interaction.user.id, cmd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a usage embed for this command
|
||||||
|
* @param {import('@structures/Command')} cmd - command object
|
||||||
|
* @param {string} prefix - guild bot prefix
|
||||||
|
* @param {string} invoke - alias that was used to trigger this command
|
||||||
|
* @param {string} [title] - the embed title
|
||||||
|
*/
|
||||||
|
getCommandUsage(cmd, prefix = PREFIX_COMMANDS.DEFAULT_PREFIX, invoke, title = "Usage") {
|
||||||
|
let desc = "";
|
||||||
|
if (cmd.command.subcommands && cmd.command.subcommands.length > 0) {
|
||||||
|
cmd.command.subcommands.forEach((sub) => {
|
||||||
|
desc += `\`${prefix}${invoke || cmd.name} ${sub.trigger}\`\n❯ ${sub.description}\n\n`;
|
||||||
|
});
|
||||||
|
if (cmd.cooldown) {
|
||||||
|
desc += `**Cooldown:** ${timeformat(cmd.cooldown)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
desc += `\`\`\`css\n${prefix}${invoke || cmd.name} ${cmd.command.usage}\`\`\``;
|
||||||
|
if (cmd.description !== "") desc += `\n**Help:** ${cmd.description}`;
|
||||||
|
if (cmd.cooldown) desc += `\n**Cooldown:** ${timeformat(cmd.cooldown)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setDescription(desc);
|
||||||
|
if (title) embed.setAuthor({ name: title });
|
||||||
|
return embed;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@structures/Command')} cmd - command object
|
||||||
|
*/
|
||||||
|
getSlashUsage(cmd) {
|
||||||
|
let desc = "";
|
||||||
|
if (cmd.slashCommand.options?.find((o) => o.type === ApplicationCommandOptionType.Subcommand)) {
|
||||||
|
const subCmds = cmd.slashCommand.options.filter((opt) => opt.type === ApplicationCommandOptionType.Subcommand);
|
||||||
|
subCmds.forEach((sub) => {
|
||||||
|
desc += `\`/${cmd.name} ${sub.name}\`\n❯ ${sub.description}\n\n`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
desc += `\`/${cmd.name}\`\n\n**Help:** ${cmd.description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.cooldown) {
|
||||||
|
desc += `\n**Cooldown:** ${timeformat(cmd.cooldown)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setDescription(desc);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} memberId
|
||||||
|
* @param {object} cmd
|
||||||
|
*/
|
||||||
|
function applyCooldown(memberId, cmd) {
|
||||||
|
const key = cmd.name + "|" + memberId;
|
||||||
|
cooldownCache.set(key, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} memberId
|
||||||
|
* @param {object} cmd
|
||||||
|
*/
|
||||||
|
function getRemainingCooldown(memberId, cmd) {
|
||||||
|
const key = cmd.name + "|" + memberId;
|
||||||
|
if (cooldownCache.has(key)) {
|
||||||
|
const remaining = (Date.now() - cooldownCache.get(key)) * 0.001;
|
||||||
|
if (remaining > cmd.cooldown) {
|
||||||
|
cooldownCache.delete(key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return cmd.cooldown - remaining;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
69
src/handlers/context.js
Normal file
69
src/handlers/context.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
const { parsePermissions } = require("@helpers/Utils");
|
||||||
|
const { timeformat } = require("@helpers/Utils");
|
||||||
|
|
||||||
|
const cooldownCache = new Map();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ContextMenuInteraction} interaction
|
||||||
|
* @param {import("@structures/BaseContext")} context
|
||||||
|
*/
|
||||||
|
handleContext: async function (interaction, context) {
|
||||||
|
// check cooldown
|
||||||
|
if (context.cooldown) {
|
||||||
|
const remaining = getRemainingCooldown(interaction.user.id, context);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `You are on cooldown, You can use this command again after ${timeformat(remaining)}`,
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check user permissions
|
||||||
|
if (interaction.member && context.userPermissions && context.userPermissions?.length > 0) {
|
||||||
|
if (!interaction.member.permissions.has(context.userPermissions)) {
|
||||||
|
return interaction.reply({
|
||||||
|
content: `You need ${parsePermissions(context.userPermissions)} to use this command`,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interaction.deferReply({ ephemeral: context.ephemeral });
|
||||||
|
await context.run(interaction);
|
||||||
|
} catch (ex) {
|
||||||
|
interaction.followUp("Oops! An error occured while running the command");
|
||||||
|
interaction.client.logger.error("contextRun", ex);
|
||||||
|
} finally {
|
||||||
|
applyCooldown(interaction.user.id, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} memberId
|
||||||
|
* @param {object} context
|
||||||
|
*/
|
||||||
|
function applyCooldown(memberId, context) {
|
||||||
|
const key = context.name + "|" + memberId;
|
||||||
|
cooldownCache.set(key, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} memberId
|
||||||
|
* @param {object} context
|
||||||
|
*/
|
||||||
|
function getRemainingCooldown(memberId, context) {
|
||||||
|
const key = context.name + "|" + memberId;
|
||||||
|
if (cooldownCache.has(key)) {
|
||||||
|
const remaining = (Date.now() - cooldownCache.get(key)) * 0.001;
|
||||||
|
if (remaining > context.cooldown) {
|
||||||
|
cooldownCache.delete(key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return context.cooldown - remaining;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
58
src/handlers/counter.js
Normal file
58
src/handlers/counter.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the counter channel for all the guildId's present in the update queue
|
||||||
|
* @param {import('@src/structures').BotClient} client
|
||||||
|
*/
|
||||||
|
async function updateCounterChannels(client) {
|
||||||
|
client.counterUpdateQueue.forEach(async (guildId) => {
|
||||||
|
const guild = client.guilds.cache.get(guildId);
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
|
||||||
|
const all = guild.memberCount;
|
||||||
|
const bots = settings.data.bots;
|
||||||
|
const members = all - bots;
|
||||||
|
|
||||||
|
for (const config of settings.counters) {
|
||||||
|
const chId = config.channel_id;
|
||||||
|
const vc = guild.channels.cache.get(chId);
|
||||||
|
if (!vc) continue;
|
||||||
|
|
||||||
|
let channelName;
|
||||||
|
if (config.counter_type.toUpperCase() === "USERS") channelName = `${config.name} : ${all}`;
|
||||||
|
if (config.counter_type.toUpperCase() === "MEMBERS") channelName = `${config.name} : ${members}`;
|
||||||
|
if (config.counter_type.toUpperCase() === "BOTS") channelName = `${config.name} : ${bots}`;
|
||||||
|
|
||||||
|
if (vc.manageable) vc.setName(channelName).catch((err) => vc.client.logger.log("Set Name error: ", err));
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
client.logger.error(`Error updating counter channels for guildId: ${guildId}`, ex);
|
||||||
|
} finally {
|
||||||
|
// remove guildId from cache
|
||||||
|
const i = client.counterUpdateQueue.indexOf(guild.id);
|
||||||
|
if (i > -1) client.counterUpdateQueue.splice(i, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize guild counters at startup
|
||||||
|
* @param {import("discord.js").Guild} guild
|
||||||
|
* @param {Object} settings
|
||||||
|
*/
|
||||||
|
async function init(guild, settings) {
|
||||||
|
if (settings.counters.find((doc) => ["MEMBERS", "BOTS"].includes(doc.counter_type.toUpperCase()))) {
|
||||||
|
const stats = await guild.fetchMemberStats();
|
||||||
|
settings.data.bots = stats[1]; // update bot count in database
|
||||||
|
await settings.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// schedule for update
|
||||||
|
if (!guild.client.counterUpdateQueue.includes(guild.id)) guild.client.counterUpdateQueue.push(guild.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, updateCounterChannels };
|
43
src/handlers/giveaway.js
Normal file
43
src/handlers/giveaway.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const { GiveawaysManager } = require("discord-giveaways");
|
||||||
|
const Model = require("@schemas/Giveaways");
|
||||||
|
|
||||||
|
class MongooseGiveaways extends GiveawaysManager {
|
||||||
|
/**
|
||||||
|
* @param {import("@structures/BotClient")} client
|
||||||
|
*/
|
||||||
|
constructor(client) {
|
||||||
|
super(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
default: {
|
||||||
|
botsCanWin: false,
|
||||||
|
embedColor: client.config.GIVEAWAYS.START_EMBED,
|
||||||
|
embedColorEnd: client.config.GIVEAWAYS.END_EMBED,
|
||||||
|
reaction: client.config.GIVEAWAYS.REACTION,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false // do not initialize manager yet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllGiveaways() {
|
||||||
|
return await Model.find().lean().exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveGiveaway(messageId, giveawayData) {
|
||||||
|
await Model.create(giveawayData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async editGiveaway(messageId, giveawayData) {
|
||||||
|
await Model.updateOne({ messageId }, giveawayData, { omitUndefined: true }).exec();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGiveaway(messageId) {
|
||||||
|
await Model.deleteOne({ messageId }).exec();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (client) => new MongooseGiveaways(client);
|
134
src/handlers/greeting.js
Normal file
134
src/handlers/greeting.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} content
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {Object} inviterData
|
||||||
|
*/
|
||||||
|
const parse = async (content, member, inviterData = {}) => {
|
||||||
|
const inviteData = {};
|
||||||
|
|
||||||
|
const getEffectiveInvites = (inviteData = {}) =>
|
||||||
|
inviteData.tracked + inviteData.added - inviteData.fake - inviteData.left || 0;
|
||||||
|
|
||||||
|
if (content.includes("{inviter:")) {
|
||||||
|
const inviterId = inviterData.member_id || "NA";
|
||||||
|
if (inviterId !== "VANITY" && inviterId !== "NA") {
|
||||||
|
try {
|
||||||
|
const inviter = await member.client.users.fetch(inviterId);
|
||||||
|
inviteData.name = inviter.username;
|
||||||
|
inviteData.tag = inviter.tag;
|
||||||
|
} catch (ex) {
|
||||||
|
member.client.logger.error(`Parsing inviterId: ${inviterId}`, ex);
|
||||||
|
inviteData.name = "NA";
|
||||||
|
inviteData.tag = "NA";
|
||||||
|
}
|
||||||
|
} else if (member.user.bot) {
|
||||||
|
inviteData.name = "OAuth";
|
||||||
|
inviteData.tag = "OAuth";
|
||||||
|
} else {
|
||||||
|
inviteData.name = inviterId;
|
||||||
|
inviteData.tag = inviterId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
.replaceAll(/\\n/g, "\n")
|
||||||
|
.replaceAll(/{server}/g, member.guild.name)
|
||||||
|
.replaceAll(/{count}/g, member.guild.memberCount)
|
||||||
|
.replaceAll(/{member:nick}/g, member.displayName)
|
||||||
|
.replaceAll(/{member:name}/g, member.user.username)
|
||||||
|
.replaceAll(/{member:dis}/g, member.user.discriminator)
|
||||||
|
.replaceAll(/{member:tag}/g, member.user.tag)
|
||||||
|
.replaceAll(/{member:mention}/g, member.toString())
|
||||||
|
.replaceAll(/{member:avatar}/g, member.displayAvatarURL())
|
||||||
|
.replaceAll(/{inviter:name}/g, inviteData.name)
|
||||||
|
.replaceAll(/{inviter:tag}/g, inviteData.tag)
|
||||||
|
.replaceAll(/{invites}/g, getEffectiveInvites(inviterData.invite_data));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {"WELCOME"|"FAREWELL"} type
|
||||||
|
* @param {Object} config
|
||||||
|
* @param {Object} inviterData
|
||||||
|
*/
|
||||||
|
const buildGreeting = async (member, type, config, inviterData) => {
|
||||||
|
if (!config) return;
|
||||||
|
let content;
|
||||||
|
|
||||||
|
// build content
|
||||||
|
if (config.content) content = await parse(config.content, member, inviterData);
|
||||||
|
|
||||||
|
// build embed
|
||||||
|
const embed = new EmbedBuilder();
|
||||||
|
if (config.embed.description) {
|
||||||
|
const parsed = await parse(config.embed.description, member, inviterData);
|
||||||
|
embed.setDescription(parsed);
|
||||||
|
}
|
||||||
|
if (config.embed.color) embed.setColor(config.embed.color);
|
||||||
|
if (config.embed.thumbnail) embed.setThumbnail(member.user.displayAvatarURL());
|
||||||
|
if (config.embed.footer) {
|
||||||
|
const parsed = await parse(config.embed.footer, member, inviterData);
|
||||||
|
embed.setFooter({ text: parsed });
|
||||||
|
}
|
||||||
|
if (config.embed.image) {
|
||||||
|
const parsed = await parse(config.embed.image, member);
|
||||||
|
embed.setImage(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default message
|
||||||
|
if (!config.content && !config.embed.description && !config.embed.footer) {
|
||||||
|
content =
|
||||||
|
type === "WELCOME"
|
||||||
|
? `Welcome to the server, ${member.displayName} 🎉`
|
||||||
|
: `${member.user.username} has left the server 👋`;
|
||||||
|
return { content };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content, embeds: [embed] };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send welcome message
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {Object} inviterData
|
||||||
|
*/
|
||||||
|
async function sendWelcome(member, inviterData = {}) {
|
||||||
|
const config = (await getSettings(member.guild))?.welcome;
|
||||||
|
if (!config || !config.enabled) return;
|
||||||
|
|
||||||
|
// check if channel exists
|
||||||
|
const channel = member.guild.channels.cache.get(config.channel);
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
// build welcome message
|
||||||
|
const response = await buildGreeting(member, "WELCOME", config, inviterData);
|
||||||
|
|
||||||
|
channel.safeSend(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send farewell message
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {Object} inviterData
|
||||||
|
*/
|
||||||
|
async function sendFarewell(member, inviterData = {}) {
|
||||||
|
const config = (await getSettings(member.guild))?.farewell;
|
||||||
|
if (!config || !config.enabled) return;
|
||||||
|
|
||||||
|
// check if channel exists
|
||||||
|
const channel = member.guild.channels.cache.get(config.channel);
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
// build farewell message
|
||||||
|
const response = await buildGreeting(member, "FAREWELL", config, inviterData);
|
||||||
|
|
||||||
|
channel.safeSend(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildGreeting,
|
||||||
|
sendWelcome,
|
||||||
|
sendFarewell,
|
||||||
|
};
|
14
src/handlers/index.js
Normal file
14
src/handlers/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
automodHandler: require("./automod"),
|
||||||
|
commandHandler: require("./command"),
|
||||||
|
contextHandler: require("./context"),
|
||||||
|
counterHandler: require("./counter"),
|
||||||
|
greetingHandler: require("./greeting"),
|
||||||
|
inviteHandler: require("./invite"),
|
||||||
|
presenceHandler: require("./presence"),
|
||||||
|
reactionRoleHandler: require("./reactionRoles"),
|
||||||
|
statsHandler: require("./stats"),
|
||||||
|
suggestionHandler: require("./suggestion"),
|
||||||
|
ticketHandler: require("./ticket"),
|
||||||
|
translationHandler: require("./translation"),
|
||||||
|
};
|
155
src/handlers/invite.js
Normal file
155
src/handlers/invite.js
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
const { Collection } = require("discord.js");
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
const { getMember } = require("@schemas/Member");
|
||||||
|
|
||||||
|
const inviteCache = new Collection();
|
||||||
|
|
||||||
|
const getInviteCache = (guild) => inviteCache.get(guild.id);
|
||||||
|
const resetInviteCache = (guild) => inviteCache.delete(guild.id);
|
||||||
|
|
||||||
|
const getEffectiveInvites = (inviteData = {}) =>
|
||||||
|
inviteData.tracked + inviteData.added - inviteData.fake - inviteData.left || 0;
|
||||||
|
|
||||||
|
const cacheInvite = (invite, isVanity) => ({
|
||||||
|
code: invite.code,
|
||||||
|
uses: invite.uses,
|
||||||
|
maxUses: invite.maxUses,
|
||||||
|
inviterId: isVanity ? "VANITY" : invite.inviter?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function caches all invites for the provided guild
|
||||||
|
* @param {import("discord.js").Guild} guild
|
||||||
|
*/
|
||||||
|
async function cacheGuildInvites(guild) {
|
||||||
|
if (!guild.members.me.permissions.has("ManageGuild")) return new Collection();
|
||||||
|
const invites = await guild.invites.fetch();
|
||||||
|
|
||||||
|
const tempMap = new Collection();
|
||||||
|
invites.forEach((inv) => tempMap.set(inv.code, cacheInvite(inv)));
|
||||||
|
if (guild.vanityURLCode) {
|
||||||
|
tempMap.set(guild.vanityURLCode, cacheInvite(await guild.fetchVanityData(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteCache.set(guild.id, tempMap);
|
||||||
|
return tempMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add roles to inviter based on invites count
|
||||||
|
* @param {import("discord.js").Guild} guild
|
||||||
|
* @param {Object} inviterData
|
||||||
|
* @param {boolean} isAdded
|
||||||
|
*/
|
||||||
|
const checkInviteRewards = async (guild, inviterData = {}, isAdded) => {
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
if (settings.invite.ranks.length > 0 && inviterData?.member_id) {
|
||||||
|
const inviter = await guild.members.fetch(inviterData?.member_id).catch(() => { });
|
||||||
|
if (!inviter) return;
|
||||||
|
|
||||||
|
const invites = getEffectiveInvites(inviterData.invite_data);
|
||||||
|
settings.invite.ranks.forEach((reward) => {
|
||||||
|
if (isAdded) {
|
||||||
|
if (invites >= reward.invites && !inviter.roles.cache.has(reward._id)) {
|
||||||
|
inviter.roles.add(reward._id);
|
||||||
|
}
|
||||||
|
} else if (invites < reward.invites && inviter.roles.cache.has(reward._id)) {
|
||||||
|
inviter.roles.remove(reward._id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track inviter by comparing new invites with cached invites
|
||||||
|
* @param {import("discord.js").GuildMember} member
|
||||||
|
*/
|
||||||
|
async function trackJoinedMember(member) {
|
||||||
|
const { guild } = member;
|
||||||
|
|
||||||
|
if (member.user.bot) return {};
|
||||||
|
|
||||||
|
const cachedInvites = inviteCache.get(guild.id);
|
||||||
|
const newInvites = await cacheGuildInvites(guild);
|
||||||
|
|
||||||
|
// return if no cached data
|
||||||
|
if (!cachedInvites) return {};
|
||||||
|
let usedInvite;
|
||||||
|
|
||||||
|
// compare newInvites with cached invites
|
||||||
|
usedInvite = newInvites.find(
|
||||||
|
(inv) => inv.uses !== 0 && cachedInvites.get(inv.code) && cachedInvites.get(inv.code).uses < inv.uses
|
||||||
|
);
|
||||||
|
|
||||||
|
// Special case: Invitation was deleted after member's arrival and
|
||||||
|
// just before GUILD_MEMBER_ADD (https://github.com/Androz2091/discord-invites-tracker/blob/29202ee8e85bb1651f19a466e2c0721b2373fefb/index.ts#L46)
|
||||||
|
if (!usedInvite) {
|
||||||
|
cachedInvites
|
||||||
|
.sort((a, b) => (a.deletedTimestamp && b.deletedTimestamp ? b.deletedTimestamp - a.deletedTimestamp : 0))
|
||||||
|
.forEach((invite) => {
|
||||||
|
if (
|
||||||
|
!newInvites.get(invite.code) && // If the invitation is no longer present
|
||||||
|
invite.maxUses > 0 && // If the invitation was indeed an invitation with a limited number of uses
|
||||||
|
invite.uses === invite.maxUses - 1 // What if the invitation was about to reach the maximum number of uses
|
||||||
|
) {
|
||||||
|
usedInvite = invite;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let inviterData = {};
|
||||||
|
if (usedInvite) {
|
||||||
|
const inviterId = usedInvite.code === guild.vanityURLCode ? "VANITY" : usedInvite.inviterId;
|
||||||
|
|
||||||
|
// log invite data
|
||||||
|
const memberDb = await getMember(guild.id, member.id);
|
||||||
|
memberDb.invite_data.inviter = inviterId;
|
||||||
|
memberDb.invite_data.code = usedInvite.code;
|
||||||
|
await memberDb.save();
|
||||||
|
|
||||||
|
// increment inviter's invites
|
||||||
|
const inviterDb = await getMember(guild.id, inviterId);
|
||||||
|
inviterDb.invite_data.tracked += 1;
|
||||||
|
await inviterDb.save();
|
||||||
|
inviterData = inviterDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInviteRewards(guild, inviterData, true);
|
||||||
|
return inviterData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch inviter data from database
|
||||||
|
* @param {import("discord.js").Guild} guild
|
||||||
|
* @param {import("discord.js").User} user
|
||||||
|
*/
|
||||||
|
async function trackLeftMember(guild, user) {
|
||||||
|
if (user.bot) return {};
|
||||||
|
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
if (!settings.invite.tracking) return;
|
||||||
|
const inviteData = (await getMember(guild.id, user.id)).invite_data;
|
||||||
|
|
||||||
|
let inviterData = {};
|
||||||
|
if (inviteData.inviter) {
|
||||||
|
const inviterId = inviteData.inviter === "VANITY" ? "VANITY" : inviteData.inviter;
|
||||||
|
const inviterDb = await getMember(guild.id, inviterId);
|
||||||
|
inviterDb.invite_data.left += 1;
|
||||||
|
await inviterDb.save();
|
||||||
|
inviterData = inviterDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInviteRewards(guild, inviterData, false);
|
||||||
|
return inviterData;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getInviteCache,
|
||||||
|
resetInviteCache,
|
||||||
|
trackJoinedMember,
|
||||||
|
trackLeftMember,
|
||||||
|
cacheGuildInvites,
|
||||||
|
checkInviteRewards,
|
||||||
|
getEffectiveInvites,
|
||||||
|
cacheInvite,
|
||||||
|
};
|
83
src/handlers/lavaclient.js
Normal file
83
src/handlers/lavaclient.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
const { EmbedBuilder } = require("discord.js");
|
||||||
|
const { Cluster } = require("lavaclient");
|
||||||
|
const prettyMs = require("pretty-ms");
|
||||||
|
const { load, SpotifyItemType } = require("@lavaclient/spotify");
|
||||||
|
require("@lavaclient/queue/register");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("@structures/BotClient")} client
|
||||||
|
*/
|
||||||
|
module.exports = (client) => {
|
||||||
|
load({
|
||||||
|
client: {
|
||||||
|
id: process.env.SPOTIFY_CLIENT_ID,
|
||||||
|
secret: process.env.SPOTIFY_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
autoResolveYoutubeTracks: false,
|
||||||
|
loaders: [SpotifyItemType.Album, SpotifyItemType.Artist, SpotifyItemType.Playlist, SpotifyItemType.Track],
|
||||||
|
});
|
||||||
|
|
||||||
|
const lavaclient = new Cluster({
|
||||||
|
nodes: client.config.MUSIC.LAVALINK_NODES,
|
||||||
|
sendGatewayPayload: (id, payload) => client.guilds.cache.get(id)?.shard?.send(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.ws.on("VOICE_SERVER_UPDATE", (data) => lavaclient.handleVoiceUpdate(data));
|
||||||
|
client.ws.on("VOICE_STATE_UPDATE", (data) => lavaclient.handleVoiceUpdate(data));
|
||||||
|
|
||||||
|
lavaclient.on("nodeConnect", (node, event) => {
|
||||||
|
client.logger.log(`Node "${node.id}" connected`);
|
||||||
|
});
|
||||||
|
|
||||||
|
lavaclient.on("nodeDisconnect", (node, event) => {
|
||||||
|
client.logger.log(`Node "${node.id}" disconnected`);
|
||||||
|
});
|
||||||
|
|
||||||
|
lavaclient.on("nodeError", (node, error) => {
|
||||||
|
client.logger.error(`Node "${node.id}" encountered an error: ${error.message}.`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
lavaclient.on("nodeDebug", (node, message) => {
|
||||||
|
client.logger.debug(`Node "${node.id}" debug: ${message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
lavaclient.on("nodeTrackStart", (_node, queue, song) => {
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: "Now Playing" })
|
||||||
|
.setColor(client.config.EMBED_COLORS.BOT_EMBED)
|
||||||
|
.setDescription(`[${song.title}](${song.uri})`)
|
||||||
|
.setFooter({ text: `Requested By: ${song.requester}` });
|
||||||
|
|
||||||
|
if (song.sourceName === "youtube") {
|
||||||
|
const identifier = song.identifier;
|
||||||
|
const thumbnail = `https://img.youtube.com/vi/${identifier}/hqdefault.jpg`;
|
||||||
|
embed.setThumbnail(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
name: "Song Duration",
|
||||||
|
value: "`" + prettyMs(song.length, { colonNotation: true }) + "`",
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (queue.tracks.length > 0) {
|
||||||
|
fields.push({
|
||||||
|
name: "Position in Queue",
|
||||||
|
value: (queue.tracks.length + 1).toString(),
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.setFields(fields);
|
||||||
|
queue.data.channel.safeSend({ embeds: [embed] });
|
||||||
|
});
|
||||||
|
|
||||||
|
lavaclient.on("nodeQueueFinish", async (_node, queue) => {
|
||||||
|
queue.data.channel.safeSend("Queue has ended.");
|
||||||
|
await client.musicManager.destroyPlayer(queue.player.guildId).then(queue.player.disconnect());
|
||||||
|
});
|
||||||
|
|
||||||
|
return lavaclient;
|
||||||
|
};
|
48
src/handlers/presence.js
Normal file
48
src/handlers/presence.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const { ActivityType } = require("discord.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@src/structures').BotClient} client
|
||||||
|
*/
|
||||||
|
function updatePresence(client) {
|
||||||
|
let message = client.config.PRESENCE.MESSAGE;
|
||||||
|
|
||||||
|
if (message.includes("{servers}")) {
|
||||||
|
message = message.replaceAll("{servers}", client.guilds.cache.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes("{members}")) {
|
||||||
|
const members = client.guilds.cache.map((g) => g.memberCount).reduce((partial_sum, a) => partial_sum + a, 0);
|
||||||
|
message = message.replaceAll("{members}", members);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "COMPETING":
|
||||||
|
return ActivityType.Competing;
|
||||||
|
|
||||||
|
case "LISTENING":
|
||||||
|
return ActivityType.Listening;
|
||||||
|
|
||||||
|
case "PLAYING":
|
||||||
|
return ActivityType.Playing;
|
||||||
|
|
||||||
|
case "WATCHING":
|
||||||
|
return ActivityType.Watching;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.user.setPresence({
|
||||||
|
status: client.config.PRESENCE.STATUS,
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
name: message,
|
||||||
|
type: getType(client.config.PRESENCE.TYPE),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function handlePresence(client) {
|
||||||
|
updatePresence(client);
|
||||||
|
setInterval(() => updatePresence(client), 10 * 60 * 1000);
|
||||||
|
};
|
46
src/handlers/reactionRoles.js
Normal file
46
src/handlers/reactionRoles.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const { getReactionRoles } = require("@schemas/ReactionRoles");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').MessageReaction} reaction
|
||||||
|
* @param {import('discord.js').User} user
|
||||||
|
*/
|
||||||
|
async handleReactionAdd(reaction, user) {
|
||||||
|
const role = await getRole(reaction);
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
|
const member = await reaction.message.guild.members.fetch(user.id);
|
||||||
|
if (!member) return;
|
||||||
|
|
||||||
|
await member.roles.add(role).catch(() => { });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').MessageReaction} reaction
|
||||||
|
* @param {import('discord.js').User} user
|
||||||
|
*/
|
||||||
|
async handleReactionRemove(reaction, user) {
|
||||||
|
const role = await getRole(reaction);
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
|
const member = await reaction.message.guild.members.fetch(user.id);
|
||||||
|
if (!member) return;
|
||||||
|
|
||||||
|
await member.roles.remove(role).catch(() => { });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').MessageReaction} reaction
|
||||||
|
*/
|
||||||
|
async function getRole(reaction) {
|
||||||
|
const { message, emoji } = reaction;
|
||||||
|
if (!message || !message.channel) return;
|
||||||
|
|
||||||
|
const rr = getReactionRoles(message.guildId, message.channelId, message.id);
|
||||||
|
const emote = emoji.id ? emoji.id : emoji.toString();
|
||||||
|
const found = rr.find((doc) => doc.emote === emote);
|
||||||
|
|
||||||
|
const reactionRole = found ? await message.guild.roles.fetch(found.role_id) : null;
|
||||||
|
return reactionRole;
|
||||||
|
}
|
121
src/handlers/stats.js
Normal file
121
src/handlers/stats.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
const { getMemberStats } = require("@schemas/MemberStats");
|
||||||
|
const { getRandomInt } = require("@helpers/Utils");
|
||||||
|
|
||||||
|
const cooldownCache = new Map();
|
||||||
|
const voiceStates = new Map();
|
||||||
|
|
||||||
|
const xpToAdd = () => getRandomInt(19) + 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} content
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {number} level
|
||||||
|
*/
|
||||||
|
const parse = (content, member, level) => {
|
||||||
|
return content
|
||||||
|
.replaceAll(/\\n/g, "\n")
|
||||||
|
.replaceAll(/{server}/g, member.guild.name)
|
||||||
|
.replaceAll(/{count}/g, member.guild.memberCount)
|
||||||
|
.replaceAll(/{member:id}/g, member.id)
|
||||||
|
.replaceAll(/{member:name}/g, member.displayName)
|
||||||
|
.replaceAll(/{member:mention}/g, member.toString())
|
||||||
|
.replaceAll(/{member:tag}/g, member.user.tag)
|
||||||
|
.replaceAll(/{level}/g, level);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* This function saves stats for a new message
|
||||||
|
* @param {import("discord.js").Message} message
|
||||||
|
* @param {boolean} isCommand
|
||||||
|
* @param {object} settings
|
||||||
|
*/
|
||||||
|
async trackMessageStats(message, isCommand, settings) {
|
||||||
|
const statsDb = await getMemberStats(message.guildId, message.member.id);
|
||||||
|
if (isCommand) statsDb.commands.prefix++;
|
||||||
|
statsDb.messages++;
|
||||||
|
|
||||||
|
// TODO: Ignore possible bot commands
|
||||||
|
|
||||||
|
// Cooldown check to prevent Message Spamming
|
||||||
|
const key = `${message.guildId}|${message.member.id}`;
|
||||||
|
if (cooldownCache.has(key)) {
|
||||||
|
const difference = (Date.now() - cooldownCache.get(key)) * 0.001;
|
||||||
|
if (difference < message.client.config.STATS.XP_COOLDOWN) {
|
||||||
|
return statsDb.save();
|
||||||
|
}
|
||||||
|
cooldownCache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update member's XP in DB
|
||||||
|
statsDb.xp += xpToAdd();
|
||||||
|
|
||||||
|
// Check if member has levelled up
|
||||||
|
let { xp, level } = statsDb;
|
||||||
|
const needed = level * level * 100;
|
||||||
|
|
||||||
|
if (xp > needed) {
|
||||||
|
level += 1;
|
||||||
|
xp -= needed;
|
||||||
|
|
||||||
|
statsDb.xp = xp;
|
||||||
|
statsDb.level = level;
|
||||||
|
let lvlUpMessage = settings.stats.xp.message;
|
||||||
|
lvlUpMessage = parse(lvlUpMessage, message.member, level);
|
||||||
|
|
||||||
|
const xpChannel = settings.stats.xp.channel && message.guild.channels.cache.get(settings.stats.xp.channel);
|
||||||
|
const lvlUpChannel = xpChannel || message.channel;
|
||||||
|
|
||||||
|
lvlUpChannel.safeSend(lvlUpMessage);
|
||||||
|
}
|
||||||
|
await statsDb.save();
|
||||||
|
cooldownCache.set(key, Date.now());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Interaction} interaction
|
||||||
|
*/
|
||||||
|
async trackInteractionStats(interaction) {
|
||||||
|
if (!interaction.guild) return;
|
||||||
|
const statsDb = await getMemberStats(interaction.guildId, interaction.member.id);
|
||||||
|
if (interaction.isChatInputCommand()) statsDb.commands.slash += 1;
|
||||||
|
if (interaction.isUserContextMenuCommand()) statsDb.contexts.user += 1;
|
||||||
|
if (interaction.isMessageContextMenuCommand()) statsDb.contexts.message += 1;
|
||||||
|
await statsDb.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').VoiceState} oldState
|
||||||
|
* @param {import('discord.js').VoiceState} newState
|
||||||
|
*/
|
||||||
|
async trackVoiceStats(oldState, newState) {
|
||||||
|
const oldChannel = oldState.channel;
|
||||||
|
const newChannel = newState.channel;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!oldChannel && !newChannel) return;
|
||||||
|
if (!newState.member) return;
|
||||||
|
|
||||||
|
const member = await newState.member.fetch().catch(() => { });
|
||||||
|
if (!member || member.user.bot) return;
|
||||||
|
|
||||||
|
// Member joined a voice channel
|
||||||
|
if (!oldChannel && newChannel) {
|
||||||
|
const statsDb = await getMemberStats(member.guild.id, member.id);
|
||||||
|
statsDb.voice.connections += 1;
|
||||||
|
await statsDb.save();
|
||||||
|
voiceStates.set(member.id, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member left a voice channel
|
||||||
|
if (oldChannel && !newChannel) {
|
||||||
|
const statsDb = await getMemberStats(member.guild.id, member.id);
|
||||||
|
if (voiceStates.has(member.id)) {
|
||||||
|
const time = now - voiceStates.get(member.id);
|
||||||
|
statsDb.voice.time += time / 1000; // add time in seconds
|
||||||
|
await statsDb.save();
|
||||||
|
voiceStates.delete(member.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
360
src/handlers/suggestion.js
Normal file
360
src/handlers/suggestion.js
Normal file
|
@ -0,0 +1,360 @@
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
const { findSuggestion, deleteSuggestionDb } = require("@schemas/Suggestions");
|
||||||
|
const { SUGGESTIONS } = require("@root/config");
|
||||||
|
|
||||||
|
const {
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ModalBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
TextInputStyle,
|
||||||
|
} = require("discord.js");
|
||||||
|
const { stripIndents } = require("common-tags");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Message} message
|
||||||
|
*/
|
||||||
|
const getStats = (message) => {
|
||||||
|
const upVotes = (message.reactions.resolve(SUGGESTIONS.EMOJI.UP_VOTE)?.count || 1) - 1;
|
||||||
|
const downVotes = (message.reactions.resolve(SUGGESTIONS.EMOJI.DOWN_VOTE)?.count || 1) - 1;
|
||||||
|
|
||||||
|
return [upVotes, downVotes];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} upVotes
|
||||||
|
* @param {number} downVotes
|
||||||
|
*/
|
||||||
|
const getVotesMessage = (upVotes, downVotes) => {
|
||||||
|
const total = upVotes + downVotes;
|
||||||
|
if (total === 0) {
|
||||||
|
return stripIndents`
|
||||||
|
_Upvotes: NA_
|
||||||
|
_Downvotes: NA_
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return stripIndents`
|
||||||
|
_Upvotes: ${upVotes} [${Math.round((upVotes / (upVotes + downVotes)) * 100)}%]_
|
||||||
|
_Downvotes: ${downVotes} [${Math.round((downVotes / (upVotes + downVotes)) * 100)}%]_
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPerms = (member, settings) => {
|
||||||
|
return (
|
||||||
|
member.permissions.has("ManageGuild") ||
|
||||||
|
member.roles.cache.find((r) => settings.suggestions.staff_roles.includes(r.id))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {import('discord.js').TextBasedChannel} channel
|
||||||
|
* @param {string} messageId
|
||||||
|
* @param {string} [reason]
|
||||||
|
*/
|
||||||
|
async function approveSuggestion(member, channel, messageId, reason) {
|
||||||
|
const { guild } = member;
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
|
||||||
|
// validate permissions
|
||||||
|
if (!hasPerms(member, settings)) return "You don't have permission to approve suggestions!";
|
||||||
|
|
||||||
|
// validate if document exists
|
||||||
|
const doc = await findSuggestion(guild.id, messageId);
|
||||||
|
if (!doc) return "Suggestion not found";
|
||||||
|
if (doc.status === "APPROVED") return "Suggestion already approved";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('discord.js').Message}
|
||||||
|
*/
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = await channel.messages.fetch({ message: messageId, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
return "Suggestion message not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonsRow = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("SUGGEST_APPROVE")
|
||||||
|
.setLabel("Approve")
|
||||||
|
.setStyle(ButtonStyle.Success)
|
||||||
|
.setDisabled(true),
|
||||||
|
new ButtonBuilder().setCustomId("SUGGEST_REJECT").setLabel("Reject").setStyle(ButtonStyle.Danger),
|
||||||
|
new ButtonBuilder().setCustomId("SUGGEST_DELETE").setLabel("Delete").setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvedEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(message.embeds[0].data.description)
|
||||||
|
.setColor(SUGGESTIONS.APPROVED_EMBED)
|
||||||
|
.setAuthor({ name: "Suggestion Approved" })
|
||||||
|
.setFooter({ text: `Approved By ${member.user.username}`, iconURL: member.displayAvatarURL() })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
// add stats if it doesn't exist
|
||||||
|
const statsField = message.embeds[0].fields.find((field) => field.name === "Stats");
|
||||||
|
if (!statsField) {
|
||||||
|
const [upVotes, downVotes] = getStats(message);
|
||||||
|
doc.stats.upvotes = upVotes;
|
||||||
|
doc.stats.downvotes = downVotes;
|
||||||
|
fields.push({ name: "Stats", value: getVotesMessage(upVotes, downVotes) });
|
||||||
|
} else {
|
||||||
|
fields.push(statsField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update reason
|
||||||
|
if (reason) fields.push({ name: "Reason", value: "```" + reason + "```" });
|
||||||
|
|
||||||
|
approvedEmbed.addFields(fields);
|
||||||
|
|
||||||
|
try {
|
||||||
|
doc.status = "APPROVED";
|
||||||
|
doc.status_updates.push({ user_id: member.id, status: "APPROVED", reason, timestamp: new Date() });
|
||||||
|
|
||||||
|
let approveChannel;
|
||||||
|
if (settings.suggestions.approved_channel) {
|
||||||
|
approveChannel = guild.channels.cache.get(settings.suggestions.approved_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestions-approve channel is not configured
|
||||||
|
if (!approveChannel) {
|
||||||
|
await message.edit({ embeds: [approvedEmbed], components: [buttonsRow] });
|
||||||
|
await message.reactions.removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestions-approve channel is configured
|
||||||
|
else {
|
||||||
|
const sent = await approveChannel.send({ embeds: [approvedEmbed], components: [buttonsRow] });
|
||||||
|
doc.channel_id = approveChannel.id;
|
||||||
|
doc.message_id = sent.id;
|
||||||
|
await message.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
await doc.save();
|
||||||
|
return "Suggestion approved";
|
||||||
|
} catch (ex) {
|
||||||
|
guild.client.logger.error("approveSuggestion", ex);
|
||||||
|
return "Failed to approve suggestion";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {import('discord.js').TextBasedChannel} channel
|
||||||
|
* @param {string} messageId
|
||||||
|
* @param {string} [reason]
|
||||||
|
*/
|
||||||
|
async function rejectSuggestion(member, channel, messageId, reason) {
|
||||||
|
const { guild } = member;
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
|
||||||
|
// validate permissions
|
||||||
|
if (!hasPerms(member, settings)) return "You don't have permission to reject suggestions!";
|
||||||
|
|
||||||
|
// validate if document exists
|
||||||
|
const doc = await findSuggestion(guild.id, messageId);
|
||||||
|
if (!doc) return "Suggestion not found";
|
||||||
|
if (doc.is_rejected) return "Suggestion already rejected";
|
||||||
|
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = await channel.messages.fetch({ message: messageId });
|
||||||
|
} catch (err) {
|
||||||
|
return "Suggestion message not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttonsRow = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId("SUGGEST_APPROVE").setLabel("Approve").setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder().setCustomId("SUGGEST_REJECT").setLabel("Reject").setStyle(ButtonStyle.Danger).setDisabled(true),
|
||||||
|
new ButtonBuilder().setCustomId("SUGGEST_DELETE").setLabel("Delete").setStyle(ButtonStyle.Secondary)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rejectedEmbed = new EmbedBuilder()
|
||||||
|
.setDescription(message.embeds[0].data.description)
|
||||||
|
.setColor(SUGGESTIONS.DENIED_EMBED)
|
||||||
|
.setAuthor({ name: "Suggestion Rejected" })
|
||||||
|
.setFooter({ text: `Rejected By ${member.user.username}`, iconURL: member.displayAvatarURL() })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
// add stats if it doesn't exist
|
||||||
|
const statsField = message.embeds[0].fields.find((field) => field.name === "Stats");
|
||||||
|
if (!statsField) {
|
||||||
|
const [upVotes, downVotes] = getStats(message);
|
||||||
|
doc.stats.upvotes = upVotes;
|
||||||
|
doc.stats.downvotes = downVotes;
|
||||||
|
fields.push({ name: "Stats", value: getVotesMessage(upVotes, downVotes) });
|
||||||
|
} else {
|
||||||
|
fields.push(statsField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update reason
|
||||||
|
if (reason) fields.push({ name: "Reason", value: "```" + reason + "```" });
|
||||||
|
|
||||||
|
rejectedEmbed.addFields(fields);
|
||||||
|
|
||||||
|
try {
|
||||||
|
doc.status = "REJECTED";
|
||||||
|
doc.status_updates.push({ user_id: member.id, status: "REJECTED", reason, timestamp: new Date() });
|
||||||
|
|
||||||
|
let rejectChannel;
|
||||||
|
if (settings.suggestions.rejected_channel) {
|
||||||
|
rejectChannel = guild.channels.cache.get(settings.suggestions.rejected_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestions-reject channel is not configured
|
||||||
|
if (!rejectChannel) {
|
||||||
|
await message.edit({ embeds: [rejectedEmbed], components: [buttonsRow] });
|
||||||
|
await message.reactions.removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// suggestions-reject channel is configured
|
||||||
|
else {
|
||||||
|
const sent = await rejectChannel.send({ embeds: [rejectedEmbed], components: [buttonsRow] });
|
||||||
|
doc.channel_id = rejectChannel.id;
|
||||||
|
doc.message_id = sent.id;
|
||||||
|
await message.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
await doc.save();
|
||||||
|
|
||||||
|
return "Suggestion rejected";
|
||||||
|
} catch (ex) {
|
||||||
|
guild.client.logger.error("rejectSuggestion", ex);
|
||||||
|
return "Failed to reject suggestion";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').GuildMember} member
|
||||||
|
* @param {import('discord.js').TextBasedChannel} channel
|
||||||
|
* @param {string} messageId
|
||||||
|
* @param {string} [reason]
|
||||||
|
*/
|
||||||
|
async function deleteSuggestion(member, channel, messageId, reason) {
|
||||||
|
const { guild } = member;
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
|
||||||
|
// validate permissions
|
||||||
|
if (!hasPerms(member, settings)) return "You don't have permission to delete suggestions!";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await channel.messages.delete(messageId);
|
||||||
|
await deleteSuggestionDb(guild.id, messageId, member.id, reason);
|
||||||
|
return "Suggestion deleted";
|
||||||
|
} catch (ex) {
|
||||||
|
guild.client.logger.error("deleteSuggestion", ex);
|
||||||
|
return "Failed to delete suggestion! Please delete manually";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ButtonInteraction} interaction
|
||||||
|
*/
|
||||||
|
async function handleApproveBtn(interaction) {
|
||||||
|
await interaction.showModal(
|
||||||
|
new ModalBuilder({
|
||||||
|
title: "Approve Suggestion",
|
||||||
|
customId: "SUGGEST_APPROVE_MODAL",
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder().addComponents([
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId("reason")
|
||||||
|
.setLabel("reason")
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setMinLength(4),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ModalSubmitInteraction} modal
|
||||||
|
*/
|
||||||
|
async function handleApproveModal(modal) {
|
||||||
|
await modal.deferReply({ ephemeral: true });
|
||||||
|
const reason = modal.fields.getTextInputValue("reason");
|
||||||
|
const response = await approveSuggestion(modal.member, modal.channel, modal.message.id, reason);
|
||||||
|
await modal.followUp(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ButtonInteraction} interaction
|
||||||
|
*/
|
||||||
|
async function handleRejectBtn(interaction) {
|
||||||
|
await interaction.showModal(
|
||||||
|
new ModalBuilder({
|
||||||
|
title: "Reject Suggestion",
|
||||||
|
customId: "SUGGEST_REJECT_MODAL",
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder().addComponents([
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId("reason")
|
||||||
|
.setLabel("reason")
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setMinLength(4),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ModalSubmitInteraction} modal
|
||||||
|
*/
|
||||||
|
async function handleRejectModal(modal) {
|
||||||
|
await modal.deferReply({ ephemeral: true });
|
||||||
|
const reason = modal.fields.getTextInputValue("reason");
|
||||||
|
const response = await rejectSuggestion(modal.member, modal.channel, modal.message.id, reason);
|
||||||
|
await modal.followUp(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ButtonInteraction} interaction
|
||||||
|
*/
|
||||||
|
async function handleDeleteBtn(interaction) {
|
||||||
|
await interaction.showModal(
|
||||||
|
new ModalBuilder({
|
||||||
|
title: "Delete Suggestion",
|
||||||
|
customId: "SUGGEST_DELETE_MODAL",
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder().addComponents([
|
||||||
|
new TextInputBuilder()
|
||||||
|
.setCustomId("reason")
|
||||||
|
.setLabel("reason")
|
||||||
|
.setStyle(TextInputStyle.Paragraph)
|
||||||
|
.setMinLength(4),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ModalSubmitInteraction} modal
|
||||||
|
*/
|
||||||
|
async function handleDeleteModal(modal) {
|
||||||
|
await modal.deferReply({ ephemeral: true });
|
||||||
|
const reason = modal.fields.getTextInputValue("reason");
|
||||||
|
const response = await deleteSuggestion(modal.member, modal.channel, modal.message.id, reason);
|
||||||
|
await modal.followUp({ content: response, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleApproveBtn,
|
||||||
|
handleApproveModal,
|
||||||
|
handleRejectBtn,
|
||||||
|
handleRejectModal,
|
||||||
|
handleDeleteBtn,
|
||||||
|
handleDeleteModal,
|
||||||
|
approveSuggestion,
|
||||||
|
rejectSuggestion,
|
||||||
|
deleteSuggestion,
|
||||||
|
};
|
307
src/handlers/ticket.js
Normal file
307
src/handlers/ticket.js
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
const {
|
||||||
|
EmbedBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ChannelType,
|
||||||
|
StringSelectMenuBuilder,
|
||||||
|
ComponentType,
|
||||||
|
} = require("discord.js");
|
||||||
|
const { TICKET } = require("@root/config.js");
|
||||||
|
|
||||||
|
// schemas
|
||||||
|
const { getSettings } = require("@schemas/Guild");
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
const { postToBin } = require("@helpers/HttpUtils");
|
||||||
|
const { error } = require("@helpers/Logger");
|
||||||
|
|
||||||
|
const OPEN_PERMS = ["ManageChannels"];
|
||||||
|
const CLOSE_PERMS = ["ManageChannels", "ReadMessageHistory"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Channel} channel
|
||||||
|
*/
|
||||||
|
function isTicketChannel(channel) {
|
||||||
|
return (
|
||||||
|
channel.type === ChannelType.GuildText &&
|
||||||
|
channel.name.startsWith("tіcket-") &&
|
||||||
|
channel.topic &&
|
||||||
|
channel.topic.startsWith("tіcket|")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Guild} guild
|
||||||
|
*/
|
||||||
|
function getTicketChannels(guild) {
|
||||||
|
return guild.channels.cache.filter((ch) => isTicketChannel(ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Guild} guild
|
||||||
|
* @param {string} userId
|
||||||
|
*/
|
||||||
|
function getExistingTicketChannel(guild, userId) {
|
||||||
|
const tktChannels = getTicketChannels(guild);
|
||||||
|
return tktChannels.filter((ch) => ch.topic.split("|")[1] === userId).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').BaseGuildTextChannel} channel
|
||||||
|
*/
|
||||||
|
async function parseTicketDetails(channel) {
|
||||||
|
if (!channel.topic) return;
|
||||||
|
const split = channel.topic?.split("|");
|
||||||
|
const userId = split[1];
|
||||||
|
const catName = split[2] || "Default";
|
||||||
|
const user = await channel.client.users.fetch(userId, { cache: false }).catch(() => { });
|
||||||
|
return { user, catName };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').BaseGuildTextChannel} channel
|
||||||
|
* @param {import('discord.js').User} closedBy
|
||||||
|
* @param {string} [reason]
|
||||||
|
*/
|
||||||
|
async function closeTicket(channel, closedBy, reason) {
|
||||||
|
if (!channel.deletable || !channel.permissionsFor(channel.guild.members.me).has(CLOSE_PERMS)) {
|
||||||
|
return "MISSING_PERMISSIONS";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await getSettings(channel.guild);
|
||||||
|
const messages = await channel.messages.fetch();
|
||||||
|
const reversed = Array.from(messages.values()).reverse();
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
reversed.forEach((m) => {
|
||||||
|
content += `[${new Date(m.createdAt).toLocaleString("en-US")}] - ${m.author.username}\n`;
|
||||||
|
if (m.cleanContent !== "") content += `${m.cleanContent}\n`;
|
||||||
|
if (m.attachments.size > 0) content += `${m.attachments.map((att) => att.proxyURL).join(", ")}\n`;
|
||||||
|
content += "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
const logsUrl = await postToBin(content, `Ticket Logs for ${channel.name}`);
|
||||||
|
const ticketDetails = await parseTicketDetails(channel);
|
||||||
|
|
||||||
|
const components = [];
|
||||||
|
if (logsUrl) {
|
||||||
|
components.push(
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setLabel("Transcript").setURL(logsUrl.short).setStyle(ButtonStyle.Link)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.deletable) await channel.delete();
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder().setAuthor({ name: "Ticket Closed" }).setColor(TICKET.CLOSE_EMBED);
|
||||||
|
const fields = [];
|
||||||
|
|
||||||
|
if (reason) fields.push({ name: "Reason", value: reason, inline: false });
|
||||||
|
fields.push(
|
||||||
|
{
|
||||||
|
name: "Opened By",
|
||||||
|
value: ticketDetails.user ? ticketDetails.user.username : "Unknown",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Closed By",
|
||||||
|
value: closedBy ? closedBy.username : "Unknown",
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
embed.setFields(fields);
|
||||||
|
|
||||||
|
// send embed to log channel
|
||||||
|
if (config.ticket.log_channel) {
|
||||||
|
const logChannel = channel.guild.channels.cache.get(config.ticket.log_channel);
|
||||||
|
logChannel.safeSend({ embeds: [embed], components });
|
||||||
|
}
|
||||||
|
|
||||||
|
// send embed to user
|
||||||
|
if (ticketDetails.user) {
|
||||||
|
const dmEmbed = embed
|
||||||
|
.setDescription(`**Server:** ${channel.guild.name}\n**Category:** ${ticketDetails.catName}`)
|
||||||
|
.setThumbnail(channel.guild.iconURL());
|
||||||
|
ticketDetails.user.send({ embeds: [dmEmbed], components }).catch((ex) => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SUCCESS";
|
||||||
|
} catch (ex) {
|
||||||
|
error("closeTicket", ex);
|
||||||
|
return "ERROR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').Guild} guild
|
||||||
|
* @param {import('discord.js').User} author
|
||||||
|
*/
|
||||||
|
async function closeAllTickets(guild, author) {
|
||||||
|
const channels = getTicketChannels(guild);
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const ch of channels) {
|
||||||
|
const status = await closeTicket(ch[1], author, "Force close all open tickets");
|
||||||
|
if (status === "SUCCESS") success += 1;
|
||||||
|
else failed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [success, failed];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("discord.js").ButtonInteraction} interaction
|
||||||
|
*/
|
||||||
|
async function handleTicketOpen(interaction) {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
const { guild, user } = interaction;
|
||||||
|
|
||||||
|
if (!guild.members.me.permissions.has(OPEN_PERMS))
|
||||||
|
return interaction.followUp(
|
||||||
|
"Cannot create ticket channel, missing `Manage Channel` permission. Contact server manager for help!"
|
||||||
|
);
|
||||||
|
|
||||||
|
const alreadyExists = getExistingTicketChannel(guild, user.id);
|
||||||
|
if (alreadyExists) return interaction.followUp(`You already have an open ticket`);
|
||||||
|
|
||||||
|
const settings = await getSettings(guild);
|
||||||
|
|
||||||
|
// limit check
|
||||||
|
const existing = getTicketChannels(guild).size;
|
||||||
|
if (existing > settings.ticket.limit) return interaction.followUp("There are too many open tickets. Try again later");
|
||||||
|
|
||||||
|
// check categories
|
||||||
|
let catName = null;
|
||||||
|
let catPerms = [];
|
||||||
|
const categories = settings.ticket.categories;
|
||||||
|
if (categories.length > 0) {
|
||||||
|
const options = [];
|
||||||
|
settings.ticket.categories.forEach((cat) => options.push({ label: cat.name, value: cat.name }));
|
||||||
|
const menuRow = new ActionRowBuilder().addComponents(
|
||||||
|
new StringSelectMenuBuilder()
|
||||||
|
.setCustomId("ticket-menu")
|
||||||
|
.setPlaceholder("Choose the ticket category")
|
||||||
|
.addOptions(options)
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.followUp({ content: "Please choose a ticket category", components: [menuRow] });
|
||||||
|
const res = await interaction.channel
|
||||||
|
.awaitMessageComponent({
|
||||||
|
componentType: ComponentType.StringSelect,
|
||||||
|
time: 60 * 1000,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.message.includes("time")) return;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) return interaction.editReply({ content: "Timed out. Try again", components: [] });
|
||||||
|
await interaction.editReply({ content: "Processing", components: [] });
|
||||||
|
catName = res.values[0];
|
||||||
|
catPerms = categories.find((cat) => cat.name === catName)?.staff_roles || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ticketNumber = (existing + 1).toString();
|
||||||
|
const permissionOverwrites = [
|
||||||
|
{
|
||||||
|
id: guild.roles.everyone,
|
||||||
|
deny: ["ViewChannel"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: guild.members.me.roles.highest.id,
|
||||||
|
allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (catPerms?.length > 0) {
|
||||||
|
catPerms?.forEach((roleId) => {
|
||||||
|
const role = guild.roles.cache.get(roleId);
|
||||||
|
if (!role) return;
|
||||||
|
permissionOverwrites.push({
|
||||||
|
id: role,
|
||||||
|
allow: ["ViewChannel", "SendMessages", "ReadMessageHistory"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tktChannel = await guild.channels.create({
|
||||||
|
name: `tіcket-${ticketNumber}`,
|
||||||
|
type: ChannelType.GuildText,
|
||||||
|
topic: `tіcket|${user.id}|${catName || "Default"}`,
|
||||||
|
permissionOverwrites,
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: `Ticket #${ticketNumber}` })
|
||||||
|
.setDescription(
|
||||||
|
`Hello ${user.toString()}
|
||||||
|
Support will be with you shortly
|
||||||
|
${catName ? `\n**Category:** ${catName}` : ""}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.setFooter({ text: "You may close your ticket anytime by clicking the button below" });
|
||||||
|
|
||||||
|
let buttonsRow = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setLabel("Close Ticket")
|
||||||
|
.setCustomId("TICKET_CLOSE")
|
||||||
|
.setEmoji("🔒")
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
);
|
||||||
|
|
||||||
|
const sent = await tktChannel.send({ content: user.toString(), embeds: [embed], components: [buttonsRow] });
|
||||||
|
|
||||||
|
const dmEmbed = new EmbedBuilder()
|
||||||
|
.setColor(TICKET.CREATE_EMBED)
|
||||||
|
.setAuthor({ name: "Ticket Created" })
|
||||||
|
.setThumbnail(guild.iconURL())
|
||||||
|
.setDescription(
|
||||||
|
`**Server:** ${guild.name}
|
||||||
|
${catName ? `**Category:** ${catName}` : ""}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setLabel("View Channel").setURL(sent.url).setStyle(ButtonStyle.Link)
|
||||||
|
);
|
||||||
|
|
||||||
|
user.send({ embeds: [dmEmbed], components: [row] }).catch((ex) => { });
|
||||||
|
|
||||||
|
await interaction.editReply(`Ticket created! 🔥`);
|
||||||
|
} catch (ex) {
|
||||||
|
error("handleTicketOpen", ex);
|
||||||
|
return interaction.editReply("Failed to create ticket channel, an error occurred!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("discord.js").ButtonInteraction} interaction
|
||||||
|
*/
|
||||||
|
async function handleTicketClose(interaction) {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
const status = await closeTicket(interaction.channel, interaction.user);
|
||||||
|
if (status === "MISSING_PERMISSIONS") {
|
||||||
|
return interaction.followUp("Cannot close the ticket, missing permissions. Contact server manager for help!");
|
||||||
|
} else if (status == "ERROR") {
|
||||||
|
return interaction.followUp("Failed to close the ticket, an error occurred!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getTicketChannels,
|
||||||
|
getExistingTicketChannel,
|
||||||
|
isTicketChannel,
|
||||||
|
closeTicket,
|
||||||
|
closeAllTickets,
|
||||||
|
handleTicketOpen,
|
||||||
|
handleTicketClose,
|
||||||
|
};
|
90
src/handlers/translation.js
Normal file
90
src/handlers/translation.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
|
||||||
|
const { isTranslated, logTranslation } = require("@schemas/TranslateLog");
|
||||||
|
const data = require("@src/data.json");
|
||||||
|
const { getLanguagesFromEmoji } = require("country-emoji-languages");
|
||||||
|
const { translate } = require("@helpers/HttpUtils");
|
||||||
|
const { timeformat } = require("@helpers/Utils");
|
||||||
|
|
||||||
|
const TRANSLATE_COOLDOWN = 120;
|
||||||
|
const cooldownCache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').User} user
|
||||||
|
*/
|
||||||
|
const getTranslationCooldown = (user) => {
|
||||||
|
if (cooldownCache.has(user.id)) {
|
||||||
|
const remaining = (Date.now() - cooldownCache.get(user.id)) * 0.001;
|
||||||
|
if (remaining > TRANSLATE_COOLDOWN) {
|
||||||
|
cooldownCache.delete(user.id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return TRANSLATE_COOLDOWN - remaining;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} emoji
|
||||||
|
* @param {import("discord.js").Message} message
|
||||||
|
* @param {import("discord.js").User} user
|
||||||
|
*/
|
||||||
|
async function handleFlagReaction(emoji, message, user) {
|
||||||
|
// cooldown check
|
||||||
|
const remaining = getTranslationCooldown(user);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return message.channel.safeSend(`${user} You must wait ${timeformat(remaining)} before translating again!`, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isTranslated(message, emoji)) return;
|
||||||
|
|
||||||
|
const languages = getLanguagesFromEmoji(emoji);
|
||||||
|
|
||||||
|
// filter languages for which google translation is available
|
||||||
|
const targetCodes = languages.filter((language) => data.GOOGLE_TRANSLATE[language] !== undefined);
|
||||||
|
if (targetCodes.length === 0) return;
|
||||||
|
|
||||||
|
// remove english if there are other language codes
|
||||||
|
if (targetCodes.length > 1 && targetCodes.includes("en")) {
|
||||||
|
targetCodes.splice(targetCodes.indexOf("en"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let src;
|
||||||
|
let desc = "";
|
||||||
|
let translated = 0;
|
||||||
|
for (const tc of targetCodes) {
|
||||||
|
const response = await translate(message.content, tc);
|
||||||
|
if (!response) continue;
|
||||||
|
src = response.inputLang;
|
||||||
|
desc += `**${response.outputLang}:**\n${response.output}\n\n`;
|
||||||
|
translated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translated === 0) return;
|
||||||
|
|
||||||
|
const btnRow = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder({
|
||||||
|
url: message.url,
|
||||||
|
label: "Original Message",
|
||||||
|
style: ButtonStyle.Link,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(message.client.config.EMBED_COLORS.BOT_EMBED)
|
||||||
|
.setAuthor({ name: `Translation from ${src}` })
|
||||||
|
.setDescription(desc)
|
||||||
|
.setFooter({
|
||||||
|
text: `Requested by ${user.username}`,
|
||||||
|
iconURL: user.displayAvatarURL(),
|
||||||
|
});
|
||||||
|
|
||||||
|
message.channel.safeSend({ embeds: [embed], components: [btnRow] }).then(
|
||||||
|
() => cooldownCache.set(user.id, Date.now()) // set cooldown
|
||||||
|
);
|
||||||
|
|
||||||
|
logTranslation(message, emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleFlagReaction,
|
||||||
|
};
|
Loading…
Reference in a new issue