Skip to content

Commit

Permalink
fix purge command and clean up council modules structure
Browse files Browse the repository at this point in the history
  • Loading branch information
WhatCats committed Aug 1, 2024
1 parent f05eede commit dfb3de0
Show file tree
Hide file tree
Showing 13 changed files with 375 additions and 361 deletions.
10 changes: 9 additions & 1 deletion src/Constants.ts
Original file line number Diff line number Diff line change
@@ -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/"

Expand Down Expand Up @@ -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"

Expand Down
15 changes: 10 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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(
Expand Down
88 changes: 43 additions & 45 deletions src/modules/council/list-members.ts
Original file line number Diff line number Diff line change
@@ -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,
})
},
})
153 changes: 153 additions & 0 deletions src/modules/council/purge-command.ts
Original file line number Diff line number Diff line change
@@ -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<TextInputBuilder>().addComponents(reason),
new ActionRowBuilder<TextInputBuilder>().addComponents(users),
new ActionRowBuilder<TextInputBuilder>().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<UserProfile>()
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<UserProfile>,
resolvable: string,
reason: string,
rankInput: string | undefined,
): Promise<string | void> {
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
User,
userMention,
} from "discord.js"

import {
ColorUtil,
Component,
Expand All @@ -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<string, number>
function getVotesValue(votes: Votes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmbedBuilder, GuildChannelCreateOptions, GuildMember, TextInputStyle } from "discord.js"
import { ButtonStyle, EmbedBuilder, GuildChannelCreateOptions, GuildMember, TextInputStyle } from "discord.js"
import {
BotMessage,
CommandHandlerInteraction,
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit dfb3de0

Please sign in to comment.