diff --git a/src/Constants.ts b/src/Constants.ts index 394da05..bdbafcb 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -1,6 +1,6 @@ import { Colors as DiscordColors } from "discord.js" import ThisColors from "./assets/colors.json" -import { PositionRole } from "./lib" +import { Permissions, PositionRole } from "./lib" export const ASSETS = process.cwd() + "/src/assets/" @@ -38,6 +38,14 @@ for (const rank of Object.values(RANKS)) { PositionRole.declarePosition(`${rank} Head`) } +export const COUNCIL_PERMISSIONS: Permissions = { + positions: Object.values(RANKS).map((rank) => `${rank} Council`), +} + +export const COUNCIL_HEAD_PERMISSIONS: Permissions = { + positions: Object.values(RANKS).map((rank) => `${rank} Head`), +} + export { default as Emojis } from "./assets/emojis.json" export { default as Links } from "./assets/links.json" diff --git a/src/index.ts b/src/index.ts index 41df0bb..ed05458 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ Settings.defaultZone = "UTC" import moduleAlias from "module-alias" moduleAlias.addAlias("lib", __dirname + "/lib/index.js") +moduleAlias.addAlias("@module", __dirname + "/modules") moduleAlias.addAlias("@Constants", __dirname + "/Constants.js") import { ASSETS, HOST_GUILD_ID } from "@Constants" @@ -16,20 +17,24 @@ const TEST = process.argv[2] === "test" function requireAll(pattern: string) { for (const path of globSync(pattern, { cwd: __dirname })) { - if (path.includes("internal") && PUBLIC) continue - if (TEST) console.log(path) - require(`./${path}`) + requirePath(path) } } +function requirePath(path: string) { + if (TEST) console.log(path) + require(`./${path}`) +} + async function main() { I18n.loadLocales(ASSETS + "lang") const intents: GatewayIntentBits[] = [] if (PUBLIC) { - requireAll("modules/exchange/**/*.js") - requireAll("modules/vouch-system/**/*.js") + requirePath("modules/vouch-system/lookup-commands.js") + requirePath("modules/vouch-system/VouchCollection.js") + requirePath("modules/vouch-system/VouchUtil.js") } else { requireAll("modules/**/*.js") intents.push( diff --git a/src/modules/council/list-members.ts b/src/modules/council/list-members.ts index e0ffc74..4d51521 100644 --- a/src/modules/council/list-members.ts +++ b/src/modules/council/list-members.ts @@ -1,45 +1,43 @@ -import { AttachmentBuilder, SlashCommandBuilder } from "discord.js" -import { SlashCommand, UserProfile } from "lib" -import { DateTime } from "luxon" - -import { RANKS } from "@Constants" -import { COUNCIL_PERMISSIONS } from "../vouch-system/internal/commands" - -const Options = { Rank: "rank" } - -SlashCommand({ - builder: new SlashCommandBuilder() - .setName("list-members") - .setDescription("List members with a certain rank.") - .setDefaultMemberPermissions("0") - .setDMPermission(false) - .addStringOption((option) => - option - .setName(Options.Rank) - .setDescription("The rank to list the members of.") - .setChoices(Object.values(RANKS).map((v) => ({ name: v, value: v }))) - .setRequired(true), - ), - - config: { permissions: COUNCIL_PERMISSIONS }, - - async handler(interaction) { - const rank = interaction.options.getString(Options.Rank, true) - const next = Object.values(RANKS)[Object.values(RANKS).indexOf(rank) + 1] - - const users = interaction.client.permissions - .getUsersWithPosition(rank) - .filter((user) => !next || !interaction.client.permissions.hasPosition(user, next)) - - const content = users.map((user) => `- ${user.username} (${user.id})`).join("\n") - const file = new AttachmentBuilder(Buffer.from(content)).setName( - `Bridge Scrims ${rank} ${DateTime.now().toFormat("dd-MM-yyyy")}.txt`, - ) - - await interaction.reply({ - content: `### ${users.length}/${UserProfile.cache.size} Members are ${rank} Rank`, - files: [file], - ephemeral: true, - }) - }, -}) +import { AttachmentBuilder, SlashCommandBuilder } from "discord.js" +import { SlashCommand, UserProfile } from "lib" +import { DateTime } from "luxon" + +import { COUNCIL_PERMISSIONS, RANKS } from "@Constants" +const Options = { Rank: "rank" } + +SlashCommand({ + builder: new SlashCommandBuilder() + .setName("list-members") + .setDescription("List members with a certain rank.") + .setDefaultMemberPermissions("0") + .setDMPermission(false) + .addStringOption((option) => + option + .setName(Options.Rank) + .setDescription("The rank to list the members of.") + .setChoices(Object.values(RANKS).map((v) => ({ name: v, value: v }))) + .setRequired(true), + ), + + config: { permissions: COUNCIL_PERMISSIONS }, + + async handler(interaction) { + const rank = interaction.options.getString(Options.Rank, true) + const next = Object.values(RANKS)[Object.values(RANKS).indexOf(rank) + 1] + + const users = interaction.client.permissions + .getUsersWithPosition(rank) + .filter((user) => !next || !interaction.client.permissions.hasPosition(user, next)) + + const content = users.map((user) => `- ${user.username} (${user.id})`).join("\n") + const file = new AttachmentBuilder(Buffer.from(content)).setName( + `Bridge Scrims ${rank} ${DateTime.now().toFormat("dd-MM-yyyy")}.txt`, + ) + + await interaction.reply({ + content: `### ${users.length}/${UserProfile.cache.size} Members are ${rank} Rank`, + files: [file], + ephemeral: true, + }) + }, +}) diff --git a/src/modules/council/purge-command.ts b/src/modules/council/purge-command.ts new file mode 100644 index 0000000..78c6a6f --- /dev/null +++ b/src/modules/council/purge-command.ts @@ -0,0 +1,153 @@ +import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, inlineCode } from "discord.js" +import { + CommandHandlerInteraction, + LocalizedSlashCommandBuilder, + MessageOptionsBuilder, + SlashCommand, + UserError, + UserProfile, + Vouch, +} from "lib" + +import { COUNCIL_HEAD_PERMISSIONS } from "@Constants" +import { LogUtil } from "@module/vouch-system/LogUtil" +import { VouchUtil } from "@module/vouch-system/VouchUtil" + +SlashCommand({ + builder: new LocalizedSlashCommandBuilder("commands.purge").setDMPermission(false), + config: { permissions: COUNCIL_HEAD_PERMISSIONS }, + async handler(interaction) { + const reason = new TextInputBuilder() + .setLabel("Reason") + .setCustomId("reason") + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(100) + + const users = new TextInputBuilder() + .setLabel("Users") + .setCustomId("users") + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setPlaceholder( + "Discord names or IDs joined by line breaks e.g.\nwhatcats\n977686340412006450\n...", + ) + + const rank = new TextInputBuilder() + .setLabel("Rank") + .setCustomId("rank") + .setStyle(TextInputStyle.Short) + .setRequired(false) + .setPlaceholder("Pristine, Prime, Private or Premium") + + await interaction.showModal( + new ModalBuilder() + .setTitle("Council Purge") + .setCustomId(interaction.path) + .addComponents( + new ActionRowBuilder().addComponents(reason), + new ActionRowBuilder().addComponents(users), + new ActionRowBuilder().addComponents(rank), + ), + ) + }, + + async handleModalSubmit(interaction) { + await interaction.deferReply({ ephemeral: true }) + + const components = interaction.components.map((v) => v.components).flat() + const reason = components.find((v) => v.customId === "reason")!.value + const users = components.find((v) => v.customId === "users")!.value.split("\n") + const rank = components.find((v) => v.customId === "rank")?.value.toLowerCase() + + const resolved = new Set() + let purged = 0 + const problems: string[] = [] + const warnings: string[] = [] + + await Promise.all( + users.map((user) => + purge(interaction, resolved, user, reason, rank) + .then((warning) => { + if (warning) warnings.push(warning) + purged++ + }) + .catch((error) => { + if (error instanceof UserError) problems.push(error.message) + else { + console.error(error) + problems.push(`Failed to purge ${inlineCode(user)} due to an unexpected error.`) + } + }), + ), + ) + + let content = `## Purged ${purged}/${users.length} User(s)` + + if (problems.length) content += `\n### Problems:` + for (const problem of problems) { + const append = `\n- ${problem}` + if (append.length + content.length > 2000) break + content += append + } + + if (content.length < 2000) { + if (warnings.length) content += `\n### Warnings:` + for (const warning of warnings) { + const append = `\n- ${warning}` + if (append.length + content.length > 2000) break + content += append + } + } + + await interaction.editReply(content) + }, +}) + +async function purge( + interaction: CommandHandlerInteraction, + resolved: Set, + resolvable: string, + reason: string, + rankInput: string | undefined, +): Promise { + const user = UserProfile.resolve(resolvable) + if (!user) throw new UserError(`User couldn't be resolved from '${resolvable}'.`) + + if (resolved.has(user)) return `Duplicate entry detected for ${user}!` + resolved.add(user) + + const rank = VouchUtil.determineDemoteRank(user, interaction.user) + if (rankInput && rank.toLowerCase() !== rankInput) { + return `${user} is wrong rank for purge (${rank}).` + } + + const removeReason = `Demoted from ${rank} by ${interaction.user.tag}.` + await interaction.client.permissions.removePosition(user, rank, removeReason) + + const vouch = await Vouch.create({ + comment: reason, + position: rank, + userId: user.id, + worth: -2, + }).catch(console.error) + + if (vouch) { + LogUtil.logCreate(vouch, interaction.user).catch(console.error) + await VouchUtil.removeSimilarVouches(vouch).catch((err) => + console.error("Failed to remove similar vouches!", err), + ) + } + + LogUtil.logDemotion(user, rank, interaction.user).catch(console.error) + + await interaction.client.users + .createDM(user.id) + .then((dm) => dm.send(`**You lost your ${rank} rank in Bridge Scrims for ${reason}.**`)) + .catch(() => null) + + const announcement = new MessageOptionsBuilder().setContent(`**${user} was removed from ${rank}.**`) + interaction.client + .buildSendMessages(`${rank} Announcements Channel`, null, announcement) + .catch(console.error) +} diff --git a/src/modules/rank-apps/CouncilVoteManager.ts b/src/modules/council/rank-apps/CouncilVoteManager.ts similarity index 95% rename from src/modules/rank-apps/CouncilVoteManager.ts rename to src/modules/council/rank-apps/CouncilVoteManager.ts index 60270fb..a7ca83c 100644 --- a/src/modules/rank-apps/CouncilVoteManager.ts +++ b/src/modules/council/rank-apps/CouncilVoteManager.ts @@ -7,6 +7,7 @@ import { User, userMention, } from "discord.js" + import { ColorUtil, Component, @@ -20,9 +21,10 @@ import { UserError, } from "lib" +import { COUNCIL_HEAD_PERMISSIONS } from "@Constants" import { EmbedBuilder } from "discord.js" import { RankAppExtras, RankAppTicketManager } from "./RankApplications" -import { COUNCIL_HEAD_PERMISSIONS, handleAccept, handleDeny } from "./app-commands" +import { handleAccept, handleDeny } from "./app-commands" export type Votes = Record function getVotesValue(votes: Votes) { diff --git a/src/modules/rank-apps/RankApplications.ts b/src/modules/council/rank-apps/RankApplications.ts similarity index 92% rename from src/modules/rank-apps/RankApplications.ts rename to src/modules/council/rank-apps/RankApplications.ts index 972a721..1b97501 100644 --- a/src/modules/rank-apps/RankApplications.ts +++ b/src/modules/council/rank-apps/RankApplications.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, GuildChannelCreateOptions, GuildMember, TextInputStyle } from "discord.js" +import { ButtonStyle, EmbedBuilder, GuildChannelCreateOptions, GuildMember, TextInputStyle } from "discord.js" import { BotMessage, CommandHandlerInteraction, @@ -14,10 +14,9 @@ import { } from "lib" import { Positions } from "@Constants" -import { ButtonStyle } from "discord.js" -import { ExchangeInputField, RecallExchangeInteraction } from "../exchange" -import { TicketCreateHandler, TicketManager } from "../tickets" -import { VouchCollection } from "../vouch-system/VouchCollection" +import { ExchangeInputField, RecallExchangeInteraction } from "@module/exchange" +import { TicketCreateHandler, TicketManager } from "@module/tickets" +import { VouchCollection } from "@module/vouch-system/VouchCollection" import { CouncilVoteManager } from "./CouncilVoteManager" export interface RankAppExtras { diff --git a/src/modules/council/rank-apps/app-commands.ts b/src/modules/council/rank-apps/app-commands.ts new file mode 100644 index 0000000..efd2134 --- /dev/null +++ b/src/modules/council/rank-apps/app-commands.ts @@ -0,0 +1,141 @@ +import { TimestampStyles, inlineCode, userMention } from "discord.js" +import { DateTime } from "luxon" + +import { + ComponentInteraction, + LocalizedError, + LocalizedSlashCommandBuilder, + MessageOptionsBuilder, + PositionRole, + ScrimsBot, + SlashCommand, + SlashCommandInteraction, + UserError, + Vouch, +} from "lib" + +import { COUNCIL_HEAD_PERMISSIONS, HOST_GUILD_ID } from "@Constants" + +import { TicketManager } from "@module/tickets" +import { AutoPromoteHandler } from "@module/vouch-system/AutoPromoteHandler" +import { LogUtil } from "@module/vouch-system/LogUtil" +import { VouchUtil } from "@module/vouch-system/VouchUtil" +import { RankAppTicketManager } from "./RankApplications" + +function fetchHostMember(resolvable: string) { + const member = + ScrimsBot.INSTANCE!.host!.members.resolve(resolvable) ?? + ScrimsBot.INSTANCE!.host!.members.cache.find( + (m) => m.user.username.toLowerCase() === resolvable.toLowerCase(), + ) + + if (!member) + throw new UserError( + `Can't complete this action since ${inlineCode(resolvable)} is not a Bridge Scrims member.`, + ) + + return member +} + +SlashCommand({ + builder: new LocalizedSlashCommandBuilder("commands.accept_app").setDMPermission(false), + config: { permissions: COUNCIL_HEAD_PERMISSIONS, defer: "ephemeral_reply" }, + handler: handleAccept, +}) + +export async function handleAccept(interaction: ComponentInteraction | SlashCommandInteraction) { + const { ticket, ticketManager } = await TicketManager.findTicket(interaction) + if (!(ticketManager instanceof RankAppTicketManager)) + throw new UserError("This command can only be used in rank application channels!") + + if (!interaction.userHasPosition(`${ticketManager.rank} Head`)) + throw new LocalizedError("command_handler.missing_permissions") + + const member = fetchHostMember(ticket.userId) + const roles = PositionRole.getPermittedRoles(ticketManager.rank, HOST_GUILD_ID) + await Promise.all( + roles.map((r) => + member.roles.add(r, `Promoted to ${ticketManager.rank} by ${interaction.user.tag}.`), + ), + ) + + const vouch = await Vouch.create({ + comment: "won vote", + position: ticketManager.rank, + userId: ticket.userId, + worth: 1, + }).catch(console.error) + + if (vouch) { + LogUtil.logCreate(vouch, interaction.user).catch(console.error) + await VouchUtil.removeSimilarVouches(vouch).catch((err) => + console.error("Failed to remove similar vouches!", err), + ) + } + + AutoPromoteHandler.announcePromotion(member.user, ticketManager.rank) + + await interaction.editReply( + new MessageOptionsBuilder() + .setContent(`${userMention(ticket.userId)} was promoted.`) + .setEphemeral(true) + .removeMentions(), + ) + + await interaction.followUp( + new MessageOptionsBuilder().setContent("This channel will now be archived...").setEphemeral(true), + ) + await ticketManager.closeTicket(ticket, interaction.user, "App Accepted") +} + +SlashCommand({ + builder: new LocalizedSlashCommandBuilder("commands.deny_app").setDMPermission(false), + config: { permissions: COUNCIL_HEAD_PERMISSIONS, defer: "ephemeral_reply" }, + handler: handleDeny, +}) + +export async function handleDeny(interaction: SlashCommandInteraction | ComponentInteraction) { + const { ticket, ticketManager } = await TicketManager.findTicket(interaction) + if (!(ticketManager instanceof RankAppTicketManager)) + throw new UserError("This command can only be used in rank application channels!") + + if (!interaction.userHasPosition(`${ticketManager.rank} Head`)) + throw new LocalizedError("command_handler.missing_permissions") + + const vouch = await Vouch.create({ + comment: "lost vote", + userId: ticket.userId, + position: ticketManager.rank, + worth: -1, + }) + + LogUtil.logCreate(vouch, interaction.user).catch(console.error) + await VouchUtil.removeSimilarVouches(vouch).catch(console.error) + + const cooldown = ticketManager.options.cooldown + const user = ticket.user() + const sent = await user + ?.send( + `:no_entry_sign: **Your ${ticketManager.rank} application was denied** since you lost your vote.` + + (cooldown + ? ` You can apply again ${DateTime.now() + .plus({ seconds: cooldown }) + .toDiscord(TimestampStyles.RelativeTime)}.` + : ""), + ) + .catch(() => false) + + await interaction.return( + new MessageOptionsBuilder() + .setContent( + `${user} was denied.` + + (!sent ? `\n:warning: Couldn't DM the user because of their privacy settings.` : ""), + ) + .removeMentions(), + ) + + await interaction.followUp( + new MessageOptionsBuilder().setContent("This channel will now be archived...").setEphemeral(true), + ) + await ticketManager.closeTicket(ticket, interaction.user, "App Denied") +} diff --git a/src/modules/rank-apps/app-commands.ts b/src/modules/rank-apps/app-commands.ts deleted file mode 100644 index 61400c2..0000000 --- a/src/modules/rank-apps/app-commands.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { - ActionRowBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, - TimestampStyles, - inlineCode, - userMention, -} from "discord.js" - -import { DateTime } from "luxon" - -import { - CommandHandlerInteraction, - ComponentInteraction, - LocalizedError, - LocalizedSlashCommandBuilder, - MessageOptionsBuilder, - Permissions, - PositionRole, - ScrimsBot, - SlashCommand, - SlashCommandInteraction, - UserError, - UserProfile, - Vouch, -} from "lib" - -import { HOST_GUILD_ID, RANKS } from "@Constants" - -import { TicketManager } from "../tickets" -import { AutoPromoteHandler } from "../vouch-system/internal/AutoPromoteHandler" -import LogUtil from "../vouch-system/internal/LogUtil" -import { VouchUtil } from "../vouch-system/VouchUtil" -import { RankAppTicketManager } from "./RankApplications" - -export const COUNCIL_HEAD_PERMISSIONS: Permissions = { - positions: Object.values(RANKS).map((rank) => `${rank} Head`), -} - -function fetchHostMember(resolvable: string) { - const member = - ScrimsBot.INSTANCE!.host!.members.resolve(resolvable) ?? - ScrimsBot.INSTANCE!.host!.members.cache.find( - (m) => m.user.username.toLowerCase() === resolvable.toLowerCase(), - ) - - if (!member) - throw new UserError( - `Can't complete this action since ${inlineCode(resolvable)} is not a Bridge Scrims member.`, - ) - - return member -} - -SlashCommand({ - builder: new LocalizedSlashCommandBuilder("commands.accept_app").setDMPermission(false), - config: { permissions: COUNCIL_HEAD_PERMISSIONS, defer: "ephemeral_reply" }, - handler: handleAccept, -}) - -export async function handleAccept(interaction: ComponentInteraction | SlashCommandInteraction) { - const { ticket, ticketManager } = await TicketManager.findTicket(interaction) - if (!(ticketManager instanceof RankAppTicketManager)) - throw new UserError("This command can only be used in rank application channels!") - - if (!interaction.userHasPosition(`${ticketManager.rank} Head`)) - throw new LocalizedError("command_handler.missing_permissions") - - const member = fetchHostMember(ticket.userId) - const roles = PositionRole.getPermittedRoles(ticketManager.rank, HOST_GUILD_ID) - await Promise.all( - roles.map((r) => - member.roles.add(r, `Promoted to ${ticketManager.rank} by ${interaction.user.tag}.`), - ), - ) - - const vouch = await Vouch.create({ - comment: "won vote", - position: ticketManager.rank, - userId: ticket.userId, - worth: 1, - }).catch(console.error) - - if (vouch) { - LogUtil.logCreate(vouch, interaction.user).catch(console.error) - await VouchUtil.removeSimilarVouches(vouch).catch((err) => - console.error("Failed to remove similar vouches!", err), - ) - } - - AutoPromoteHandler.announcePromotion(member.user, ticketManager.rank) - - await interaction.editReply( - new MessageOptionsBuilder() - .setContent(`${userMention(ticket.userId)} was promoted.`) - .setEphemeral(true) - .removeMentions(), - ) - - await interaction.followUp( - new MessageOptionsBuilder().setContent("This channel will now be archived...").setEphemeral(true), - ) - await ticketManager.closeTicket(ticket, interaction.user, "App Accepted") -} - -SlashCommand({ - builder: new LocalizedSlashCommandBuilder("commands.deny_app").setDMPermission(false), - config: { permissions: COUNCIL_HEAD_PERMISSIONS, defer: "ephemeral_reply" }, - handler: handleDeny, -}) - -export async function handleDeny(interaction: SlashCommandInteraction | ComponentInteraction) { - const { ticket, ticketManager } = await TicketManager.findTicket(interaction) - if (!(ticketManager instanceof RankAppTicketManager)) - throw new UserError("This command can only be used in rank application channels!") - - if (!interaction.userHasPosition(`${ticketManager.rank} Head`)) - throw new LocalizedError("command_handler.missing_permissions") - - const vouch = await Vouch.create({ - comment: "lost vote", - userId: ticket.userId, - position: ticketManager.rank, - worth: -1, - }) - - LogUtil.logCreate(vouch, interaction.user).catch(console.error) - await VouchUtil.removeSimilarVouches(vouch).catch(console.error) - - const cooldown = ticketManager.options.cooldown - const user = ticket.user() - const sent = await user - ?.send( - `:no_entry_sign: **Your ${ticketManager.rank} application was denied** since you lost your vote.` + - (cooldown - ? ` You can apply again ${DateTime.now() - .plus({ seconds: cooldown }) - .toDiscord(TimestampStyles.RelativeTime)}.` - : ""), - ) - .catch(() => false) - - await interaction.return( - new MessageOptionsBuilder() - .setContent( - `${user} was denied.` + - (!sent ? `\n:warning: Couldn't DM the user because of their privacy settings.` : ""), - ) - .removeMentions(), - ) - - await interaction.followUp( - new MessageOptionsBuilder().setContent("This channel will now be archived...").setEphemeral(true), - ) - await ticketManager.closeTicket(ticket, interaction.user, "App Denied") -} - -SlashCommand({ - builder: new LocalizedSlashCommandBuilder("commands.purge").setDMPermission(false), - config: { permissions: COUNCIL_HEAD_PERMISSIONS }, - async handler(interaction) { - const reason = new TextInputBuilder() - .setLabel("Reason") - .setCustomId("reason") - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setMaxLength(100) - - const users = new TextInputBuilder() - .setLabel("Users") - .setCustomId("users") - .setStyle(TextInputStyle.Paragraph) - .setRequired(true) - .setPlaceholder( - "Discord names or IDs joined by line breaks e.g.\nwhatcats\n977686340412006450\n...", - ) - - const rank = new TextInputBuilder() - .setLabel("Rank") - .setCustomId("rank") - .setStyle(TextInputStyle.Short) - .setRequired(false) - .setPlaceholder("Pristine, Prime, Private or Premium") - - await interaction.showModal( - new ModalBuilder() - .setTitle("Council Purge") - .setCustomId(interaction.path) - .addComponents( - new ActionRowBuilder().addComponents(reason), - new ActionRowBuilder().addComponents(users), - new ActionRowBuilder().addComponents(rank), - ), - ) - }, - - async handleModalSubmit(interaction) { - await interaction.deferReply({ ephemeral: true }) - - const components = interaction.components.map((v) => v.components).flat() - const reason = components.find((v) => v.customId === "reason")!.value - const users = components.find((v) => v.customId === "users")!.value.split("\n") - const rank = components.find((v) => v.customId === "rank")?.value.toLowerCase() - - const resolved = new Set() - const problems: string[] = [] - const warnings: string[] = [] - - await Promise.all( - users.map((user) => { - purge(interaction, resolved, user, reason, rank) - .then((warning) => { - if (warning) warnings.push(warning) - }) - .catch((error) => { - if (error instanceof UserError) problems.push(error.message) - else { - console.error(error) - problems.push(`Failed to purge ${inlineCode(user)} due to an unexpected error.`) - } - }) - }), - ) - - let content = `## Purged ${users.length - problems.length}/${users.length} User(s)` - - if (problems.length) content += `\n### Problems:` - for (const problem of problems) { - const append = `\n- ${problem}` - if (append.length + content.length > 2000) break - content += append - } - - if (content.length < 2000) { - if (warnings.length) content += `\n### Warnings:` - for (const warning of warnings) { - const append = `\n- ${warning}` - if (append.length + content.length > 2000) break - content += append - } - } - - await interaction.editReply(content) - }, -}) - -async function purge( - interaction: CommandHandlerInteraction, - resolved: Set, - resolvable: string, - reason: string, - rankInput: string | undefined, -): Promise { - const user = UserProfile.resolve(resolvable) - if (!user) throw new UserError(`User couldn't be resolved from '${resolvable}'.`) - - if (resolved.has(user)) return `Duplicate entry detected for ${user}!` - resolved.add(user) - - const rank = VouchUtil.determineDemoteRank(user, interaction.user) - if (rankInput && rank.toLowerCase() !== rankInput) { - return `${user} is wrong rank for purge (${rank}).` - } - - const removeReason = `Demoted from ${rank} by ${interaction.user.tag}.` - await interaction.client.permissions.removePosition(user, rank, removeReason) - - const vouch = await Vouch.create({ - comment: reason, - position: rank, - userId: user.id, - worth: -2, - }).catch(console.error) - - if (vouch) { - LogUtil.logCreate(vouch, interaction.user).catch(console.error) - await VouchUtil.removeSimilarVouches(vouch).catch((err) => - console.error("Failed to remove similar vouches!", err), - ) - } - - LogUtil.logDemotion(user, rank, interaction.user).catch(console.error) - const dm = await interaction.client.users.createDM(user.id).catch(() => null) - if (dm != null) dm.send(`**You lost your ${rank} rank in Bridge Scrims for ${reason}.**`) - - const announcement = new MessageOptionsBuilder().setContent(`**${user} was removed from ${rank}.**`) - interaction.client - .buildSendMessages(`${rank} Announcements Channel`, null, announcement) - .catch(console.error) -} diff --git a/src/modules/vouch-system/internal/AutoPromoteHandler.ts b/src/modules/vouch-system/AutoPromoteHandler.ts similarity index 95% rename from src/modules/vouch-system/internal/AutoPromoteHandler.ts rename to src/modules/vouch-system/AutoPromoteHandler.ts index 8cf35b0..b4280ca 100644 --- a/src/modules/vouch-system/internal/AutoPromoteHandler.ts +++ b/src/modules/vouch-system/AutoPromoteHandler.ts @@ -2,7 +2,7 @@ import { User } from "discord.js" import { HOST_GUILD_ID, RANKS, ROLE_APP_HUB } from "@Constants" import { Config, I18n, MessageOptionsBuilder, PositionRole, ScrimsBot, Vouch } from "lib" -import { VouchCollection } from "../VouchCollection" +import { VouchCollection } from "./VouchCollection" const PROMOTIONS_CHANNEL = Config.declareType("Promotions Channel") diff --git a/src/modules/vouch-system/internal/LogUtil.ts b/src/modules/vouch-system/LogUtil.ts similarity index 95% rename from src/modules/vouch-system/internal/LogUtil.ts rename to src/modules/vouch-system/LogUtil.ts index ad2c785..52fcc31 100644 --- a/src/modules/vouch-system/internal/LogUtil.ts +++ b/src/modules/vouch-system/LogUtil.ts @@ -11,7 +11,7 @@ import { } from "lib" import { Colors, RANKS } from "@Constants" -import { VouchUtil } from "../VouchUtil" +import { VouchUtil } from "./VouchUtil" Object.values(RANKS).forEach((rank) => { Config.declareType(`${rank} Vouch Log Channel`) @@ -25,7 +25,7 @@ const Emojis = { Purge: ":flag_white:", } -export default class LogUtil { +export class LogUtil { static async logDelete(vouch: Vouch, executor: User) { await ScrimsBot.INSTANCE?.buildSendLogMessages( `${vouch.position} Vouch Log Channel`, diff --git a/src/modules/vouch-system/public-commands.ts b/src/modules/vouch-system/lookup-commands.ts similarity index 100% rename from src/modules/vouch-system/public-commands.ts rename to src/modules/vouch-system/lookup-commands.ts diff --git a/src/modules/vouch-system/internal/commands.ts b/src/modules/vouch-system/modify-commands.ts similarity index 93% rename from src/modules/vouch-system/internal/commands.ts rename to src/modules/vouch-system/modify-commands.ts index 8430cc8..088d1bc 100644 --- a/src/modules/vouch-system/internal/commands.ts +++ b/src/modules/vouch-system/modify-commands.ts @@ -15,18 +15,17 @@ import { ContextMenuInteraction, LocalizedSlashCommandBuilder, MessageOptionsBuilder, - Permissions, SlashCommand, SlashCommandInteraction, UserError, Vouch, } from "lib" -import { Positions, RANKS } from "@Constants" -import { VouchCollection } from "../VouchCollection" -import { VouchUtil } from "../VouchUtil" +import { COUNCIL_PERMISSIONS, Positions, RANKS } from "@Constants" import { AutoPromoteHandler } from "./AutoPromoteHandler" -import LogUtil from "./LogUtil" +import { LogUtil } from "./LogUtil" +import { VouchCollection } from "./VouchCollection" +import { VouchUtil } from "./VouchUtil" const Options = { User: "user", @@ -35,9 +34,6 @@ const Options = { } const STAFF_PERMISSIONS = { positionLevel: Positions.Staff } -export const COUNCIL_PERMISSIONS: Permissions = { - positions: Object.values(RANKS).map((rank) => `${rank} Council`), -} function buildRankOption(command: string) { return new SlashCommandStringOption() @@ -154,7 +150,8 @@ SlashCommand({ option .setRequired(false) .setName(Options.Comment) - .setNameAndDescription("commands.vouch.comment_option"), + .setNameAndDescription("commands.vouch.comment_option") + .setMaxLength(500), ) .setDMPermission(false), @@ -178,7 +175,8 @@ SlashCommand({ option .setRequired(false) .setName(Options.Comment) - .setNameAndDescription("commands.devouch.comment_option"), + .setNameAndDescription("commands.devouch.comment_option") + .setMaxLength(500), ) .setDMPermission(false), diff --git a/tsconfig.json b/tsconfig.json index 61d8f5d..d1191f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "outDir": "dist", "paths": { "lib": ["./src/lib/index.ts"], + "@module/*": ["./src/modules/*"], "@Constants": ["./src/Constants.ts"] } },