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