From 035960fdcc4538339350d6e320557c744adcf40c Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Mon, 18 Mar 2024 11:19:14 +0200 Subject: [PATCH 01/17] Setup internal channel mapping ### Notes This adds an additional feature to the new audit channel flow. Once an `ext-name-audit` channel is setup, now you can make an `int-name-audit` channel, which will perform these steps: 1. Setup a new role `Defense Internal: Name` 2. Generate an invite for the `int-name-audit` channel 3. Check if a matching `ext-name-audit` channel exists, if so assign `ViewChannel` permission to `Defense Internal: name` role. --- discord-scripts/invite-management.ts | 52 ++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 0354bdf6..f8ae726e 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -3,6 +3,7 @@ import { Client, TextChannel } from "discord.js" import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts" const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?.*)-audit$/ +const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?.*)-audit$/ async function createInvite( channel: TextChannel, @@ -81,13 +82,21 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { channel.parent && channel.parent.name === "defense" && channel instanceof TextChannel && - EXTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name) + (EXTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name) || + INTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name)) ) { + const auditChannelType: string = EXTERNAL_AUDIT_CHANNEL_REGEXP.test( + channel.name, + ) + ? "External" + : "Internal" try { const defenseInvite = await createInvite(channel) if (defenseInvite) { robot.logger.info( - `New invite created for defense audit channel: ${channel.name}, URL: ${defenseInvite.url}`, + `New invite created for defense ${auditChannelType.toLowerCase()} audit channel: ${ + channel.name + }, URL: ${defenseInvite.url}`, ) channel.send( `Here is your invite link: ${ @@ -108,9 +117,9 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { .join(" ") if (clientName) { - const roleName = clientName - ? `Defense: ${clientName}` - : `Defense: ${channel.name}` + const roleName = `Defense ${auditChannelType}: ${ + clientName || channel.name + }` const role = await channel.guild.roles.create({ name: roleName, @@ -120,6 +129,37 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { await channel.permissionOverwrites.create(role, { ViewChannel: true, }) + + if (auditChannelType === "Internal") { + const externalAuditChannel = channel.guild.channels.cache.find( + (c) => + c.name === `🔒ext-${clientName.toLowerCase()}-audit` && + c.parent && + c.parent.name === "defense", + ) as TextChannel + + if (externalAuditChannel) { + await externalAuditChannel.permissionOverwrites.create( + role.id, + { + ViewChannel: true, + }, + ) + channel.send( + `**${role.name}** role granted ViewChannel access to the external audit channel **${externalAuditChannel.name}**`, + ) + robot.logger.info( + `ViewChannel access granted to ${role.name} for external audit channel ${externalAuditChannel.name}`, + ) + } else { + channel.send("No matching external audit channel found.") + robot.logger.info( + "No matching external audit channel found for " + + `ext-${clientName.toLowerCase()}-audit`, + ) + } + } + channel.send( `**${role.name}** role created and permissions set for **${channel.name}**`, ) @@ -133,7 +173,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } } catch (error) { robot.logger.error( - `An error occurred setting up the defense audit channel: ${error}`, + `An error occurred setting up the defense ${auditChannelType.toLowerCase()} audit channel: ${error}`, ) } } From 4263db81c9f69c3fd9993efe9d0a240f79a3d2ca Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Mon, 18 Mar 2024 15:45:03 +0200 Subject: [PATCH 02/17] WIP Invite management This is still a work in progress just testing out the ability of assigning a role to an invite based of the uses it has. --- discord-scripts/invite-management.ts | 90 +++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index f8ae726e..c56b30f2 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -1,9 +1,10 @@ import { Robot } from "hubot" -import { Client, TextChannel } from "discord.js" +import { Client, Collection, TextChannel, GuildMember } from "discord.js" import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts" const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?.*)-audit$/ const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?.*)-audit$/ +const guildInvites = new Collection() async function createInvite( channel: TextChannel, @@ -27,6 +28,20 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { const { application } = discordClient if (application) { + // stores a list of all invites on runtime + setTimeout(async () => { + discordClient.guilds.cache.forEach(async (guild) => { + const fetchInvites = await guild.invites.fetch() + guildInvites.set( + guild.id, + new Collection( + fetchInvites.map((invite) => [invite.code, invite.uses]), + ), + ) + robot.logger.info("List all guild invites:", guildInvites) + }) + }, 1000) + // Check if create-invite command already exists, if not create it const existingInviteCommand = (await application.commands.fetch()).find( (command) => command.name === "create-invite", @@ -178,5 +193,78 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } } }) + + // WIP, just testing out invite counting in order to verify which invite was used on join + discordClient.on("guildMemberAdd", async (member: GuildMember) => { + // for debugging + robot.logger.info(member) + + const oldInvites = + (guildInvites.get(member.guild.id) as Collection< + string, + { uses: number } + >) || new Collection() + const fetchedInvites = await member.guild.invites.fetch() + const newInvites = new Collection( + fetchedInvites.map((invite) => [ + invite.code, + { uses: invite.uses ?? 0 }, + ]), + ) + guildInvites.set(member.guild.id, newInvites) + + robot.logger.info( + `Old Invites: ${JSON.stringify(Array.from(oldInvites.entries()))}`, + ) + robot.logger.info( + `New Invites: ${JSON.stringify(Array.from(newInvites.entries()))}`, + ) + + const usedInvite = fetchedInvites.find((fetchedInvite) => { + const oldUseCount = oldInvites.get(fetchedInvite.code)?.uses ?? 0 + return (fetchedInvite.uses ?? 0) > oldUseCount + }) + + robot.logger.info(`Used Invite: ${usedInvite ? usedInvite.code : "None"}`) + + if (usedInvite && usedInvite.channelId) { + const channel = member.guild.channels.cache.get( + usedInvite.channelId, + ) as TextChannel + if (channel) { + const channelTypeMatch = channel.name.match(/(ext|int)-(.*)-audit/) + const auditChannelType = channelTypeMatch + ? channelTypeMatch[1] === "ext" + ? "External" + : "Internal" + : null + const clientName = channelTypeMatch ? channelTypeMatch[2] : "" + robot.logger.info(`Channel Name: ${channelTypeMatch}`) + robot.logger.info(`Audit Channel Type: ${auditChannelType}`) + + if (auditChannelType) { + const roleName = `Defense ${auditChannelType}: ${clientName}` + const role = member.guild.roles.cache.find( + (r) => + r.name + .toLowerCase() + .includes(`defense ${auditChannelType}`.toLowerCase()) && + r.name.toLowerCase().includes(clientName.toLowerCase()), + ) + + if (role) { + await member.roles.add(role) + robot.logger.info( + `Assigned role ${roleName} to ${member.displayName}`, + ) + } else { + robot.logger.info(`Role ${roleName} not found in guild.`) + } + } + } + } else { + robot.logger.info("Could not find which invite was used.") + } + }) } } From e0012601142ec7627b26c2329b0ceb60ad47be8f Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 19 Mar 2024 10:38:46 +0200 Subject: [PATCH 03/17] Fix role mapping from invite Rewrite some of the checks and counts in-order to correctly match the role. The previous method was breaking on different format channels such as `ext-thesis-test-audit` --- discord-scripts/invite-management.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index c56b30f2..e8cc7d56 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -221,8 +221,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ) const usedInvite = fetchedInvites.find((fetchedInvite) => { - const oldUseCount = oldInvites.get(fetchedInvite.code)?.uses ?? 0 - return (fetchedInvite.uses ?? 0) > oldUseCount + const oldInvite = oldInvites.get(fetchedInvite.code) + return (fetchedInvite.uses ?? 0) > (oldInvite ? oldInvite.uses : 0) }) robot.logger.info(`Used Invite: ${usedInvite ? usedInvite.code : "None"}`) @@ -238,18 +238,23 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ? "External" : "Internal" : null - const clientName = channelTypeMatch ? channelTypeMatch[2] : "" - robot.logger.info(`Channel Name: ${channelTypeMatch}`) + const clientName = channelTypeMatch + ? channelTypeMatch[2].replace(/-/g, " ") + : "" + robot.logger.info(`Channel Name: ${channel.name}`) robot.logger.info(`Audit Channel Type: ${auditChannelType}`) if (auditChannelType) { - const roleName = `Defense ${auditChannelType}: ${clientName}` + const fixedClientName = clientName + .split(" ") + .map( + (word) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(" ") + const roleName = `Defense ${auditChannelType}: ${fixedClientName}` const role = member.guild.roles.cache.find( - (r) => - r.name - .toLowerCase() - .includes(`defense ${auditChannelType}`.toLowerCase()) && - r.name.toLowerCase().includes(clientName.toLowerCase()), + (r) => r.name.toLowerCase() === roleName.toLowerCase(), ) if (role) { From b7e2a64ba6ea587f382bac73a3818a8084d74fbe Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 19 Mar 2024 10:58:36 +0200 Subject: [PATCH 04/17] Move listInvites Moves `listInvites` into it's own function for easier calls --- discord-scripts/invite-management.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index e8cc7d56..77bfe1ab 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -24,22 +24,27 @@ async function createInvite( } } +async function listInvites(discordClient: Client, robot: Robot): Promise { + discordClient.guilds.cache.forEach(async (guild) => { + const fetchInvites = await guild.invites.fetch().catch(error => { + robot.logger.error(`Failed to fetch invites for guild ${guild.name}: ${error}`) + }) + + if (fetchInvites) { + guildInvites.set(guild.id, new Collection(fetchInvites.map(invite => [invite.code, invite.uses]))) + // just for debugging + robot.logger.info(`List all guild invites for ${guild.name}:`, guildInvites.get(guild.id)) + } + }) +} + export default async function sendInvite(discordClient: Client, robot: Robot) { const { application } = discordClient if (application) { // stores a list of all invites on runtime setTimeout(async () => { - discordClient.guilds.cache.forEach(async (guild) => { - const fetchInvites = await guild.invites.fetch() - guildInvites.set( - guild.id, - new Collection( - fetchInvites.map((invite) => [invite.code, invite.uses]), - ), - ) - robot.logger.info("List all guild invites:", guildInvites) - }) + await listInvites(discordClient, robot) }, 1000) // Check if create-invite command already exists, if not create it @@ -121,6 +126,9 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } days and has a maximum of ${defenseInvite.maxUses} uses.`, ) } + // store new invites + await listInvites(discordClient, robot) + // Create a new role with the client name extracted and set permissions to that channel const clientName = channel.name .split("-") From 62015eb62bdd8e824e26b0e6df2775c296bf1200 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 19 Mar 2024 11:37:39 +0200 Subject: [PATCH 05/17] Fix lint errors Refactor the `guildMemberAdd` event to resolve lint expression errors and make sure channel name is found correctly based on ext, int patterns --- discord-scripts/invite-management.ts | 49 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 77bfe1ab..465561dd 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -26,14 +26,24 @@ async function createInvite( async function listInvites(discordClient: Client, robot: Robot): Promise { discordClient.guilds.cache.forEach(async (guild) => { - const fetchInvites = await guild.invites.fetch().catch(error => { - robot.logger.error(`Failed to fetch invites for guild ${guild.name}: ${error}`) + const fetchInvites = await guild.invites.fetch().catch((error) => { + robot.logger.error( + `Failed to fetch invites for guild ${guild.name}: ${error}`, + ) }) if (fetchInvites) { - guildInvites.set(guild.id, new Collection(fetchInvites.map(invite => [invite.code, invite.uses]))) + guildInvites.set( + guild.id, + new Collection( + fetchInvites.map((invite) => [invite.code, invite.uses]), + ), + ) // just for debugging - robot.logger.info(`List all guild invites for ${guild.name}:`, guildInvites.get(guild.id)) + robot.logger.info( + `List all guild invites for ${guild.name}:`, + guildInvites.get(guild.id), + ) } }) } @@ -241,26 +251,25 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ) as TextChannel if (channel) { const channelTypeMatch = channel.name.match(/(ext|int)-(.*)-audit/) - const auditChannelType = channelTypeMatch - ? channelTypeMatch[1] === "ext" - ? "External" - : "Internal" - : null const clientName = channelTypeMatch - ? channelTypeMatch[2].replace(/-/g, " ") + ? channelTypeMatch[2] + .replace(/-/g, " ") + .split(" ") + .map( + (word) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(" ") : "" + robot.logger.info(`Channel Name: ${channel.name}`) - robot.logger.info(`Audit Channel Type: ${auditChannelType}`) - if (auditChannelType) { - const fixedClientName = clientName - .split(" ") - .map( - (word) => - word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), - ) - .join(" ") - const roleName = `Defense ${auditChannelType}: ${fixedClientName}` + if (channelTypeMatch) { + const auditType = + channelTypeMatch[1] === "ext" ? "External" : "Internal" + robot.logger.info(`Audit Channel Type: ${auditType}`) + const roleName = `Defense ${auditType}: ${clientName}` + const role = member.guild.roles.cache.find( (r) => r.name.toLowerCase() === roleName.toLowerCase(), ) From 0fcf9625210420e6c566d36e6ed130ee47eeea93 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 19 Mar 2024 11:55:09 +0200 Subject: [PATCH 06/17] Fix role assignment issue Resolves the issue that would fail to assign internal roles to external channel if the channel name had the format `ext-client-name-audit` vs `ext-clientname-audit` --- discord-scripts/invite-management.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 465561dd..abc56f71 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -164,9 +164,12 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { }) if (auditChannelType === "Internal") { + const normalizedClientName = clientName + .replace(/\s+/g, "-") + .toLowerCase() const externalAuditChannel = channel.guild.channels.cache.find( (c) => - c.name === `🔒ext-${clientName.toLowerCase()}-audit` && + c.name === `🔒ext-${normalizedClientName}-audit` && c.parent && c.parent.name === "defense", ) as TextChannel From 18eab2295f235f82fdc86ec354d1b7526f8e71cc Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 19 Mar 2024 12:38:01 +0200 Subject: [PATCH 07/17] Resolve invite use count bug This fixes the bug happening on invites not counting correctly if they were being used more than 2 times on certain edge cases. --- discord-scripts/invite-management.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index abc56f71..5b09c2e7 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -226,11 +226,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { { uses: number } >) || new Collection() const fetchedInvites = await member.guild.invites.fetch() - const newInvites = new Collection( - fetchedInvites.map((invite) => [ - invite.code, - { uses: invite.uses ?? 0 }, - ]), + const newInvites = new Collection( + fetchedInvites.map((invite) => [invite.code, invite.uses ?? 0]), ) guildInvites.set(member.guild.id, newInvites) @@ -243,7 +240,9 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { const usedInvite = fetchedInvites.find((fetchedInvite) => { const oldInvite = oldInvites.get(fetchedInvite.code) - return (fetchedInvite.uses ?? 0) > (oldInvite ? oldInvite.uses : 0) + const oldUses = + typeof oldInvite === "object" ? oldInvite.uses : oldInvite + return (fetchedInvite.uses ?? 0) > (oldUses ?? 0) }) robot.logger.info(`Used Invite: ${usedInvite ? usedInvite.code : "None"}`) From 487162bdfcdade592cb6762e09fa2dafa4e3a0be Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 20 Mar 2024 11:50:36 +0200 Subject: [PATCH 08/17] WIP Add `defense-audit` command This is still WIP but adds the `defense-audit` command with the parameter `client-name` to automatically build `int-clientname-audit` and `ext-clientname-audit` channels with two roles `Defense Internal: ClientName` and `Defense External: ClientName`. Can only be run from the `defense` category --- discord-scripts/invite-management.ts | 325 ++++++++++++++++++--------- 1 file changed, 225 insertions(+), 100 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 5b09c2e7..df794ed8 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -1,9 +1,16 @@ import { Robot } from "hubot" -import { Client, Collection, TextChannel, GuildMember } from "discord.js" +import { + ApplicationCommandOptionType, + Client, + ChannelType, + Collection, + TextChannel, + GuildMember, +} from "discord.js" import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts" -const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?.*)-audit$/ -const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?.*)-audit$/ +// const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?.*)-audit$/ +// const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?.*)-audit$/ const guildInvites = new Collection() async function createInvite( @@ -69,6 +76,28 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { }) robot.logger.info("create invite command set") } + + // Check if defense-audit command exists, if not create it + const existingDefenseCommand = (await application.commands.fetch()).find( + (command) => command.name === "defense-audit", + ) + if (existingDefenseCommand === undefined) { + robot.logger.info("No defense-audit command found, creating it!") + await application.commands.create({ + name: "defense-audit", + description: "Creates Defense audit channels", + options: [ + { + name: "client-name", + type: ApplicationCommandOptionType.String, + description: "The name of the client to create the channels for", + required: true, + }, + ], + }) + robot.logger.info("Defense audit command set") + } + // Create an invite based of the command and channel where the command has been run discordClient.on("interactionCreate", async (interaction) => { if ( @@ -106,115 +135,211 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } }) - // Generates an invite if the channel name matches ext-*-audit format - discordClient.on("channelCreate", async (channel) => { + // Create the defense audit channels and roles based off the command + discordClient.on("interactionCreate", async (interaction) => { if ( - channel.parent && - channel.parent.name === "defense" && - channel instanceof TextChannel && - (EXTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name) || - INTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name)) + !interaction.isCommand() || + interaction.commandName !== "defense-audit" ) { - const auditChannelType: string = EXTERNAL_AUDIT_CHANNEL_REGEXP.test( - channel.name, - ) - ? "External" - : "Internal" - try { - const defenseInvite = await createInvite(channel) - if (defenseInvite) { - robot.logger.info( - `New invite created for defense ${auditChannelType.toLowerCase()} audit channel: ${ - channel.name - }, URL: ${defenseInvite.url}`, - ) - channel.send( - `Here is your invite link: ${ - defenseInvite.url - }\nThis invite expires in ${ - (defenseInvite.maxAge / DAY) * MILLISECOND - } days and has a maximum of ${defenseInvite.maxUses} uses.`, - ) - } - // store new invites - await listInvites(discordClient, robot) - - // Create a new role with the client name extracted and set permissions to that channel - const clientName = channel.name - .split("-") - .slice(1, -1) - .map( - (segment) => - segment.substring(0, 1).toUpperCase() + segment.substring(1), - ) - .join(" ") + return + } - if (clientName) { - const roleName = `Defense ${auditChannelType}: ${ - clientName || channel.name - }` + if (!interaction.guild) { + await interaction.reply({ + content: "This command can only be used in a server.", + ephemeral: true, + }) + return + } - const role = await channel.guild.roles.create({ - name: roleName, - reason: `Role for ${channel.name} channel`, - }) + const clientName = interaction.options.get("client-name") + if (!clientName) { + await interaction.reply({ + content: "Client name is required for the defense-audit command.", + ephemeral: true, + }) + return + } - await channel.permissionOverwrites.create(role, { - ViewChannel: true, - }) + try { + if (typeof clientName.value === "string") { + const normalizedClientName = clientName.value + .replace(/\s+/g, "-") + .toLowerCase() + const internalChannelName = `int-${normalizedClientName}-audit` + const externalChannelName = `ext-${normalizedClientName}-audit` - if (auditChannelType === "Internal") { - const normalizedClientName = clientName - .replace(/\s+/g, "-") - .toLowerCase() - const externalAuditChannel = channel.guild.channels.cache.find( - (c) => - c.name === `🔒ext-${normalizedClientName}-audit` && - c.parent && - c.parent.name === "defense", - ) as TextChannel - - if (externalAuditChannel) { - await externalAuditChannel.permissionOverwrites.create( - role.id, - { - ViewChannel: true, - }, - ) - channel.send( - `**${role.name}** role granted ViewChannel access to the external audit channel **${externalAuditChannel.name}**`, - ) - robot.logger.info( - `ViewChannel access granted to ${role.name} for external audit channel ${externalAuditChannel.name}`, - ) - } else { - channel.send("No matching external audit channel found.") - robot.logger.info( - "No matching external audit channel found for " + - `ext-${clientName.toLowerCase()}-audit`, - ) - } - } + const defenseCategory = interaction.guild.channels.cache.find( + (c) => c.name === "defense", + ) - channel.send( - `**${role.name}** role created and permissions set for **${channel.name}**`, - ) - robot.logger.info( - `${role.name} role created and permissions set for channel ${channel.name}`, - ) - } else { - robot.logger.info( - `Skipping role creation due to empty client name for channel ${channel.name}`, - ) + if (!defenseCategory) { + await interaction.reply({ + content: "Defense category does not exist.", + ephemeral: true, + }) + return } - } catch (error) { - robot.logger.error( - `An error occurred setting up the defense ${auditChannelType.toLowerCase()} audit channel: ${error}`, - ) + + const internalChannel = await interaction.guild.channels.create({ + name: internalChannelName, + type: ChannelType.GuildText, + parent: defenseCategory.id, + }) + + const externalChannel = await interaction.guild.channels.create({ + name: externalChannelName, + type: ChannelType.GuildText, + parent: defenseCategory.id, + }) + + const internalRoleName = `Defense Internal: ${clientName.value}` + const externalRoleName = `Defense External: ${clientName.value}` + const internalRole = await interaction.guild.roles.create({ + name: internalRoleName, + reason: "Role for internal audit channel", + }) + const externalRole = await interaction.guild.roles.create({ + name: externalRoleName, + reason: "Role for external audit channel", + }) + + await internalChannel.permissionOverwrites.create(internalRole, { + ViewChannel: true, + }) + await externalChannel.permissionOverwrites.create(externalRole, { + ViewChannel: true, + }) + await externalChannel.permissionOverwrites.create(internalRole, { + ViewChannel: true, + }) + + const internalInvite = await createInvite(internalChannel) + const externalInvite = await createInvite(externalChannel) + + await interaction.reply({ + content: `Defense audit setup complete for: ${clientName}\n\nInternal Channel Invite: ${internalInvite.url}\nExternal Channel Invite: ${externalInvite.url}`, + ephemeral: true, + }) } + } catch (error) { + robot.logger.error(error) + await interaction.reply({ + content: "An error occurred while setting up the defense audit.", + ephemeral: true, + }) } }) + // // Generates an invite if the channel name matches ext-*-audit format + // discordClient.on("channelCreate", async (channel) => { + // if ( + // channel.parent && + // channel.parent.name === "defense" && + // channel instanceof TextChannel && + // (EXTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name) || + // INTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name)) + // ) { + // const auditChannelType: string = EXTERNAL_AUDIT_CHANNEL_REGEXP.test( + // channel.name, + // ) + // ? "External" + // : "Internal" + // try { + // const defenseInvite = await createInvite(channel) + // if (defenseInvite) { + // robot.logger.info( + // `New invite created for defense ${auditChannelType.toLowerCase()} audit channel: ${ + // channel.name + // }, URL: ${defenseInvite.url}`, + // ) + // channel.send( + // `Here is your invite link: ${ + // defenseInvite.url + // }\nThis invite expires in ${ + // (defenseInvite.maxAge / DAY) * MILLISECOND + // } days and has a maximum of ${defenseInvite.maxUses} uses.`, + // ) + // } + // // store new invites + // await listInvites(discordClient, robot) + + // // Create a new role with the client name extracted and set permissions to that channel + // const clientName = channel.name + // .split("-") + // .slice(1, -1) + // .map( + // (segment) => + // segment.substring(0, 1).toUpperCase() + segment.substring(1), + // ) + // .join(" ") + + // if (clientName) { + // const roleName = `Defense ${auditChannelType}: ${ + // clientName || channel.name + // }` + + // const role = await channel.guild.roles.create({ + // name: roleName, + // reason: `Role for ${channel.name} channel`, + // }) + + // await channel.permissionOverwrites.create(role, { + // ViewChannel: true, + // }) + + // if (auditChannelType === "Internal") { + // const normalizedClientName = clientName + // .replace(/\s+/g, "-") + // .toLowerCase() + // const externalAuditChannel = channel.guild.channels.cache.find( + // (c) => + // c.name === `🔒ext-${normalizedClientName}-audit` && + // c.parent && + // c.parent.name === "defense", + // ) as TextChannel + + // if (externalAuditChannel) { + // await externalAuditChannel.permissionOverwrites.create( + // role.id, + // { + // ViewChannel: true, + // }, + // ) + // channel.send( + // `**${role.name}** role granted ViewChannel access to the external audit channel **${externalAuditChannel.name}**`, + // ) + // robot.logger.info( + // `ViewChannel access granted to ${role.name} for external audit channel ${externalAuditChannel.name}`, + // ) + // } else { + // channel.send("No matching external audit channel found.") + // robot.logger.info( + // "No matching external audit channel found for " + + // `ext-${clientName.toLowerCase()}-audit`, + // ) + // } + // } + + // channel.send( + // `**${role.name}** role created and permissions set for **${channel.name}**`, + // ) + // robot.logger.info( + // `${role.name} role created and permissions set for channel ${channel.name}`, + // ) + // } else { + // robot.logger.info( + // `Skipping role creation due to empty client name for channel ${channel.name}`, + // ) + // } + // } catch (error) { + // robot.logger.error( + // `An error occurred setting up the defense ${auditChannelType.toLowerCase()} audit channel: ${error}`, + // ) + // } + // } + // }) + // WIP, just testing out invite counting in order to verify which invite was used on join discordClient.on("guildMemberAdd", async (member: GuildMember) => { // for debugging From 0c3bd080cb73ac58220a32060f6305aad50e5033 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 20 Mar 2024 14:00:15 +0200 Subject: [PATCH 09/17] Command restrictions This commit makes it so the `defense-audit` command can only be run within the `defense` parent category of channels and will send a reply if it isn't working as it should. Also adds an `interaction.editReply` inorder to avoid any timeout issues happening --- discord-scripts/invite-management.ts | 133 +++++---------------------- 1 file changed, 21 insertions(+), 112 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index df794ed8..cfad0bbc 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -152,6 +152,20 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { return } + if ( + !interaction.guild || + !(interaction.channel instanceof TextChannel) || + (interaction.channel.parent && + interaction.channel.parent.name !== "defense") + ) { + await interaction.reply({ + content: + "This command can only be run in channels under the 'defense' category.", + ephemeral: true, + }) + return + } + const clientName = interaction.options.get("client-name") if (!clientName) { await interaction.reply({ @@ -163,6 +177,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { try { if (typeof clientName.value === "string") { + await interaction.deferReply({ ephemeral: true }) const normalizedClientName = clientName.value .replace(/\s+/g, "-") .toLowerCase() @@ -217,9 +232,12 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { const internalInvite = await createInvite(internalChannel) const externalInvite = await createInvite(externalChannel) - await interaction.reply({ - content: `Defense audit setup complete for: ${clientName}\n\nInternal Channel Invite: ${internalInvite.url}\nExternal Channel Invite: ${externalInvite.url}`, - ephemeral: true, + await interaction.editReply({ + content: + `**Defense audit setup complete for: ${clientName.value}**\n\n` + + `Internal Channel: <#${internalChannel.id}> (Invite: ${internalInvite.url})\n` + + `External Channel: <#${externalChannel.id}> (Invite: ${externalInvite.url})\n\n` + + `Roles created: <@&${internalRole.id}>, <@&${externalRole.id}>`, }) } } catch (error) { @@ -231,115 +249,6 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } }) - // // Generates an invite if the channel name matches ext-*-audit format - // discordClient.on("channelCreate", async (channel) => { - // if ( - // channel.parent && - // channel.parent.name === "defense" && - // channel instanceof TextChannel && - // (EXTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name) || - // INTERNAL_AUDIT_CHANNEL_REGEXP.test(channel.name)) - // ) { - // const auditChannelType: string = EXTERNAL_AUDIT_CHANNEL_REGEXP.test( - // channel.name, - // ) - // ? "External" - // : "Internal" - // try { - // const defenseInvite = await createInvite(channel) - // if (defenseInvite) { - // robot.logger.info( - // `New invite created for defense ${auditChannelType.toLowerCase()} audit channel: ${ - // channel.name - // }, URL: ${defenseInvite.url}`, - // ) - // channel.send( - // `Here is your invite link: ${ - // defenseInvite.url - // }\nThis invite expires in ${ - // (defenseInvite.maxAge / DAY) * MILLISECOND - // } days and has a maximum of ${defenseInvite.maxUses} uses.`, - // ) - // } - // // store new invites - // await listInvites(discordClient, robot) - - // // Create a new role with the client name extracted and set permissions to that channel - // const clientName = channel.name - // .split("-") - // .slice(1, -1) - // .map( - // (segment) => - // segment.substring(0, 1).toUpperCase() + segment.substring(1), - // ) - // .join(" ") - - // if (clientName) { - // const roleName = `Defense ${auditChannelType}: ${ - // clientName || channel.name - // }` - - // const role = await channel.guild.roles.create({ - // name: roleName, - // reason: `Role for ${channel.name} channel`, - // }) - - // await channel.permissionOverwrites.create(role, { - // ViewChannel: true, - // }) - - // if (auditChannelType === "Internal") { - // const normalizedClientName = clientName - // .replace(/\s+/g, "-") - // .toLowerCase() - // const externalAuditChannel = channel.guild.channels.cache.find( - // (c) => - // c.name === `🔒ext-${normalizedClientName}-audit` && - // c.parent && - // c.parent.name === "defense", - // ) as TextChannel - - // if (externalAuditChannel) { - // await externalAuditChannel.permissionOverwrites.create( - // role.id, - // { - // ViewChannel: true, - // }, - // ) - // channel.send( - // `**${role.name}** role granted ViewChannel access to the external audit channel **${externalAuditChannel.name}**`, - // ) - // robot.logger.info( - // `ViewChannel access granted to ${role.name} for external audit channel ${externalAuditChannel.name}`, - // ) - // } else { - // channel.send("No matching external audit channel found.") - // robot.logger.info( - // "No matching external audit channel found for " + - // `ext-${clientName.toLowerCase()}-audit`, - // ) - // } - // } - - // channel.send( - // `**${role.name}** role created and permissions set for **${channel.name}**`, - // ) - // robot.logger.info( - // `${role.name} role created and permissions set for channel ${channel.name}`, - // ) - // } else { - // robot.logger.info( - // `Skipping role creation due to empty client name for channel ${channel.name}`, - // ) - // } - // } catch (error) { - // robot.logger.error( - // `An error occurred setting up the defense ${auditChannelType.toLowerCase()} audit channel: ${error}`, - // ) - // } - // } - // }) - // WIP, just testing out invite counting in order to verify which invite was used on join discordClient.on("guildMemberAdd", async (member: GuildMember) => { // for debugging From 971b6dab4d895f4ecf762e1f605e8155b561d73a Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 20 Mar 2024 15:56:45 +0200 Subject: [PATCH 10/17] Remove debugging Removes the debug loggers from the scripts, not needed anymore! --- discord-scripts/invite-management.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index cfad0bbc..36cbd4e5 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -9,8 +9,6 @@ import { } from "discord.js" import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts" -// const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?.*)-audit$/ -// const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?.*)-audit$/ const guildInvites = new Collection() async function createInvite( @@ -46,11 +44,6 @@ async function listInvites(discordClient: Client, robot: Robot): Promise { fetchInvites.map((invite) => [invite.code, invite.uses]), ), ) - // just for debugging - robot.logger.info( - `List all guild invites for ${guild.name}:`, - guildInvites.get(guild.id), - ) } }) } @@ -265,13 +258,6 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ) guildInvites.set(member.guild.id, newInvites) - robot.logger.info( - `Old Invites: ${JSON.stringify(Array.from(oldInvites.entries()))}`, - ) - robot.logger.info( - `New Invites: ${JSON.stringify(Array.from(newInvites.entries()))}`, - ) - const usedInvite = fetchedInvites.find((fetchedInvite) => { const oldInvite = oldInvites.get(fetchedInvite.code) const oldUses = From 042c39150f7f52748fb8e3fd3eb8b2cbf3a0ed8d Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 20 Mar 2024 16:02:03 +0200 Subject: [PATCH 11/17] Remove extra debugger --- discord-scripts/invite-management.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 36cbd4e5..d325e6a5 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -242,11 +242,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { } }) - // WIP, just testing out invite counting in order to verify which invite was used on join + // Check list of invites and compare when a new user joins which invite code has been used, then assign role based on channel.name.match TO DO: Modify this to work with potentially all invites discordClient.on("guildMemberAdd", async (member: GuildMember) => { - // for debugging - robot.logger.info(member) - const oldInvites = (guildInvites.get(member.guild.id) as Collection< string, From 982cbc5f0ec43baf301759831a1662fee84b30b4 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 12 Apr 2024 10:46:26 +0300 Subject: [PATCH 12/17] Review updates / fixes This commit adds a number of changes to the audit channel flow. mainly: - Update permissions if channel already exists - Check if role already exists, if so skip - Changed logic so that if audit channels already exist, we output a different message to interaction - Remove embeds by wrapping invite URL in quotes. - Change verbiage around messages - Separates InternalChannel and externalChannel - Changed invite code permission match --- discord-scripts/invite-management.ts | 177 +++++++++++++++++---------- 1 file changed, 112 insertions(+), 65 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index d325e6a5..87e79c04 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -31,19 +31,20 @@ async function createInvite( async function listInvites(discordClient: Client, robot: Robot): Promise { discordClient.guilds.cache.forEach(async (guild) => { - const fetchInvites = await guild.invites.fetch().catch((error) => { + try { + const fetchInvites = await guild.invites.fetch() + if (fetchInvites) { + guildInvites.set( + guild.id, + new Collection( + fetchInvites.map((invite) => [invite.code, invite.uses]), + ), + ) + } + } catch (error) { robot.logger.error( `Failed to fetch invites for guild ${guild.name}: ${error}`, ) - }) - - if (fetchInvites) { - guildInvites.set( - guild.id, - new Collection( - fetchInvites.map((invite) => [invite.code, invite.uses]), - ), - ) } }) } @@ -72,7 +73,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { // Check if defense-audit command exists, if not create it const existingDefenseCommand = (await application.commands.fetch()).find( - (command) => command.name === "defense-audit", + (command) => command.name === "defense-audit2", ) if (existingDefenseCommand === undefined) { robot.logger.info("No defense-audit command found, creating it!") @@ -83,7 +84,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { { name: "client-name", type: ApplicationCommandOptionType.String, - description: "The name of the client to create the channels for", + description: + "The name of the audit/client to create the channels for.", required: true, }, ], @@ -153,7 +155,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ) { await interaction.reply({ content: - "This command can only be run in channels under the 'defense' category.", + "This command can only be run in chat channels under the 'Defense' category.", ephemeral: true, }) return @@ -172,13 +174,13 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { if (typeof clientName.value === "string") { await interaction.deferReply({ ephemeral: true }) const normalizedClientName = clientName.value - .replace(/\s+/g, "-") + .replace(/[^a-zA-Z0-9]/g, "-") .toLowerCase() - const internalChannelName = `int-${normalizedClientName}-audit` - const externalChannelName = `ext-${normalizedClientName}-audit` + const internalChannelName = `🔒int-${normalizedClientName}-audit` + const externalChannelName = `🔒ext-${normalizedClientName}-audit` const defenseCategory = interaction.guild.channels.cache.find( - (c) => c.name === "defense", + (category) => category.name === "defense", ) if (!defenseCategory) { @@ -189,49 +191,98 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { return } - const internalChannel = await interaction.guild.channels.create({ - name: internalChannelName, - type: ChannelType.GuildText, - parent: defenseCategory.id, - }) - - const externalChannel = await interaction.guild.channels.create({ - name: externalChannelName, - type: ChannelType.GuildText, - parent: defenseCategory.id, - }) + // Internal channel setup + let internalChannel = interaction.guild.channels.cache.find( + (channel) => channel.name === internalChannelName, + ) as TextChannel + const internalChannelCreated = !internalChannel + if (internalChannelCreated) { + internalChannel = await interaction.guild.channels.create({ + name: internalChannelName, + type: ChannelType.GuildText, + parent: defenseCategory.id, + }) + } const internalRoleName = `Defense Internal: ${clientName.value}` - const externalRoleName = `Defense External: ${clientName.value}` - const internalRole = await interaction.guild.roles.create({ - name: internalRoleName, - reason: "Role for internal audit channel", - }) - const externalRole = await interaction.guild.roles.create({ - name: externalRoleName, - reason: "Role for external audit channel", - }) - - await internalChannel.permissionOverwrites.create(internalRole, { - ViewChannel: true, - }) - await externalChannel.permissionOverwrites.create(externalRole, { - ViewChannel: true, - }) - await externalChannel.permissionOverwrites.create(internalRole, { - ViewChannel: true, - }) + let internalRole = interaction.guild.roles.cache.find( + (r) => r.name === internalRoleName, + ) + if (!internalRole) { + internalRole = await interaction.guild.roles.create({ + name: internalRoleName, + reason: "Role for internal audit channel", + }) + } + if (internalChannel) { + await internalChannel.permissionOverwrites.create(internalRole, { + ViewChannel: true, + }) + await internalChannel.send( + `@here **Welcome to the ${clientName.value} Internal Audit Channel!**`, + ) + } const internalInvite = await createInvite(internalChannel) + + // External channel setup + let externalChannel = interaction.guild.channels.cache.find( + (channel) => channel.name === externalChannelName, + ) as TextChannel + const externalChannelCreated = !externalChannel + if (externalChannelCreated) { + externalChannel = await interaction.guild.channels.create({ + name: externalChannelName, + type: ChannelType.GuildText, + parent: defenseCategory.id, + }) + } + + const externalRoleName = `Defense External: ${clientName.value}` + let externalRole = interaction.guild.roles.cache.find( + (r) => r.name === externalRoleName, + ) + if (!externalRole) { + externalRole = await interaction.guild.roles.create({ + name: externalRoleName, + reason: "Role for external audit channel", + }) + } + + if (externalChannel) { + await externalChannel.permissionOverwrites.create(externalRole, { + ViewChannel: true, + }) + await externalChannel.permissionOverwrites.create(internalRole, { + ViewChannel: true, + }) + await externalChannel.send( + `@here **Welcome to the ${clientName.value} External Audit Channel!**`, + ) + } const externalInvite = await createInvite(externalChannel) - await interaction.editReply({ - content: - `**Defense audit setup complete for: ${clientName.value}**\n\n` + - `Internal Channel: <#${internalChannel.id}> (Invite: ${internalInvite.url})\n` + - `External Channel: <#${externalChannel.id}> (Invite: ${externalInvite.url})\n\n` + - `Roles created: <@&${internalRole.id}>, <@&${externalRole.id}>`, - }) + // Final interaction response + if (internalChannelCreated || externalChannelCreated) { + await interaction.editReply({ + content: + `**Defense audit setup complete for: ${clientName.value}**\n\n` + + `Internal Channel: <#${internalChannel.id}> (Invite: \`${internalInvite.url}\`)\n` + + `External Channel: <#${externalChannel.id}> (Invite: \`${externalInvite.url}\`)\n\n` + + `Roles created: <@&${internalRole.id}>, <@&${externalRole.id}>`, + }) + } else { + await interaction.editReply({ + content: + `**Defense audit channels already set up for: ${clientName.value}**\n\n` + + `These channels were found here:\n` + + `- Internal Channel: <#${internalChannel.id}> (Invite: \`${internalInvite.url}\`)\n` + + `- External Channel: <#${externalChannel.id}> (Invite: \`${externalInvite.url}\`)\n\n` + + `We've updated permissions to these roles:\n` + + `- Internal Role: <@&${internalRole.id}>\n` + + `- External Role: <@&${externalRole.id}>`, + }) + } } } catch (error) { robot.logger.error(error) @@ -262,8 +313,6 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { return (fetchedInvite.uses ?? 0) > (oldUses ?? 0) }) - robot.logger.info(`Used Invite: ${usedInvite ? usedInvite.code : "None"}`) - if (usedInvite && usedInvite.channelId) { const channel = member.guild.channels.cache.get( usedInvite.channelId, @@ -281,26 +330,24 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { .join(" ") : "" - robot.logger.info(`Channel Name: ${channel.name}`) - if (channelTypeMatch) { const auditType = channelTypeMatch[1] === "ext" ? "External" : "Internal" - robot.logger.info(`Audit Channel Type: ${auditType}`) const roleName = `Defense ${auditType}: ${clientName}` const role = member.guild.roles.cache.find( (r) => r.name.toLowerCase() === roleName.toLowerCase(), ) - if (role) { await member.roles.add(role) - robot.logger.info( - `Assigned role ${roleName} to ${member.displayName}`, - ) - } else { - robot.logger.info(`Role ${roleName} not found in guild.`) } + robot.logger.info( + `Invite code used: ${ + usedInvite ? usedInvite.code : "None" + }, Username joined: ${ + member.displayName + }, Role assignments: ${roleName}`, + ) } } } else { From 62e5c891fd2f883fbaadceda108b5365eeac6311 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 12 Apr 2024 10:51:31 +0300 Subject: [PATCH 13/17] Lint fix --- discord-scripts/invite-management.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 87e79c04..0ca30e42 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -275,10 +275,10 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { await interaction.editReply({ content: `**Defense audit channels already set up for: ${clientName.value}**\n\n` + - `These channels were found here:\n` + + "These channels were found here:\n" + `- Internal Channel: <#${internalChannel.id}> (Invite: \`${internalInvite.url}\`)\n` + `- External Channel: <#${externalChannel.id}> (Invite: \`${externalInvite.url}\`)\n\n` + - `We've updated permissions to these roles:\n` + + "We've updated permissions to these roles:\n" + `- Internal Role: <@&${internalRole.id}>\n` + `- External Role: <@&${externalRole.id}>`, }) From 71cd2f645ea42ea9c0bf44516228a29448e24bca Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 12 Apr 2024 10:58:32 +0300 Subject: [PATCH 14/17] Rename to`audit-name` --- discord-scripts/invite-management.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 0ca30e42..a995a317 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -82,7 +82,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { description: "Creates Defense audit channels", options: [ { - name: "client-name", + name: "audit-name", type: ApplicationCommandOptionType.String, description: "The name of the audit/client to create the channels for.", @@ -161,7 +161,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { return } - const clientName = interaction.options.get("client-name") + const clientName = interaction.options.get("audit-name") if (!clientName) { await interaction.reply({ content: "Client name is required for the defense-audit command.", From 3eb7e8bfb8c4fedd78e7afe1ca4d023f2df37c9c Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 16 Apr 2024 16:22:40 +0300 Subject: [PATCH 15/17] Switch to objects This commit switches over from discordjs `Collection` in favour of storing invites as an object. --- discord-scripts/invite-management.ts | 45 +++++++++++++--------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index a995a317..0f3e3ce6 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -3,13 +3,12 @@ import { ApplicationCommandOptionType, Client, ChannelType, - Collection, TextChannel, GuildMember, } from "discord.js" import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts" -const guildInvites = new Collection() +const guildInvites: { [guildId: string]: { [inviteCode: string]: number } } = {} async function createInvite( channel: TextChannel, @@ -34,12 +33,11 @@ async function listInvites(discordClient: Client, robot: Robot): Promise { try { const fetchInvites = await guild.invites.fetch() if (fetchInvites) { - guildInvites.set( - guild.id, - new Collection( - fetchInvites.map((invite) => [invite.code, invite.uses]), - ), - ) + guildInvites[guild.id] = guildInvites[guild.id] || {} + + fetchInvites.forEach((invite) => { + guildInvites[guild.id][invite.code] = invite.uses ?? 0 + }) } } catch (error) { robot.logger.error( @@ -267,8 +265,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { await interaction.editReply({ content: `**Defense audit setup complete for: ${clientName.value}**\n\n` + - `Internal Channel: <#${internalChannel.id}> (Invite: \`${internalInvite.url}\`)\n` + - `External Channel: <#${externalChannel.id}> (Invite: \`${externalInvite.url}\`)\n\n` + + `Internal Channel: <#${internalChannel.id}> - Invite: \`${internalInvite.url}\`\n` + + `External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n\n` + `Roles created: <@&${internalRole.id}>, <@&${externalRole.id}>`, }) } else { @@ -276,8 +274,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { content: `**Defense audit channels already set up for: ${clientName.value}**\n\n` + "These channels were found here:\n" + - `- Internal Channel: <#${internalChannel.id}> (Invite: \`${internalInvite.url}\`)\n` + - `- External Channel: <#${externalChannel.id}> (Invite: \`${externalInvite.url}\`)\n\n` + + `- Internal Channel: <#${internalChannel.id}> - Invite: \`${internalInvite.url}\`\n` + + `- External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n\n` + "We've updated permissions to these roles:\n" + `- Internal Role: <@&${internalRole.id}>\n` + `- External Role: <@&${externalRole.id}>`, @@ -295,22 +293,19 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { // Check list of invites and compare when a new user joins which invite code has been used, then assign role based on channel.name.match TO DO: Modify this to work with potentially all invites discordClient.on("guildMemberAdd", async (member: GuildMember) => { - const oldInvites = - (guildInvites.get(member.guild.id) as Collection< - string, - { uses: number } - >) || new Collection() + const oldInvites = guildInvites[member.guild.id] || {} const fetchedInvites = await member.guild.invites.fetch() - const newInvites = new Collection( - fetchedInvites.map((invite) => [invite.code, invite.uses ?? 0]), - ) - guildInvites.set(member.guild.id, newInvites) + + const newInvites: { [code: string]: number } = {} + fetchedInvites.forEach((invite) => { + newInvites[invite.code] = invite.uses ?? 0 + }) + + guildInvites[member.guild.id] = newInvites const usedInvite = fetchedInvites.find((fetchedInvite) => { - const oldInvite = oldInvites.get(fetchedInvite.code) - const oldUses = - typeof oldInvite === "object" ? oldInvite.uses : oldInvite - return (fetchedInvite.uses ?? 0) > (oldUses ?? 0) + const oldUses = oldInvites[fetchedInvite.code] || 0 + return (fetchedInvite.uses ?? 0) > oldUses }) if (usedInvite && usedInvite.channelId) { From ec9974ba42b20d818a42096f3afdc97416871be2 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 16 Apr 2024 16:26:21 +0300 Subject: [PATCH 16/17] Small fix --- discord-scripts/invite-management.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index 0f3e3ce6..a3d73a47 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -71,7 +71,7 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { // Check if defense-audit command exists, if not create it const existingDefenseCommand = (await application.commands.fetch()).find( - (command) => command.name === "defense-audit2", + (command) => command.name === "defense-audit", ) if (existingDefenseCommand === undefined) { robot.logger.info("No defense-audit command found, creating it!") From 8bc32cb5eacdb033f27571920224825bf90fae04 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Mon, 22 Apr 2024 16:07:32 +0300 Subject: [PATCH 17/17] Add invite uses / expiry This commit does some refactoring based off review feedback (thank you!) as well as making sure the invite uses / expiry date is listed properly. --- discord-scripts/invite-management.ts | 38 +++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/discord-scripts/invite-management.ts b/discord-scripts/invite-management.ts index a3d73a47..0993f9ef 100644 --- a/discord-scripts/invite-management.ts +++ b/discord-scripts/invite-management.ts @@ -33,7 +33,7 @@ async function listInvites(discordClient: Client, robot: Robot): Promise { try { const fetchInvites = await guild.invites.fetch() if (fetchInvites) { - guildInvites[guild.id] = guildInvites[guild.id] || {} + guildInvites[guild.id] ??= {} fetchInvites.forEach((invite) => { guildInvites[guild.id][invite.code] = invite.uses ?? 0 @@ -193,8 +193,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { let internalChannel = interaction.guild.channels.cache.find( (channel) => channel.name === internalChannelName, ) as TextChannel - const internalChannelCreated = !internalChannel - if (internalChannelCreated) { + const internalChannelNeedsCreation = !internalChannel + if (internalChannelNeedsCreation) { internalChannel = await interaction.guild.channels.create({ name: internalChannelName, type: ChannelType.GuildText, @@ -213,22 +213,28 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { }) } + const internalInvite = await createInvite(internalChannel) + const internalInviteExpiry = Math.floor( + Date.now() / 1000 + internalInvite.maxAge, + ) + if (internalChannel) { await internalChannel.permissionOverwrites.create(internalRole, { ViewChannel: true, }) await internalChannel.send( - `@here **Welcome to the ${clientName.value} Internal Audit Channel!**`, + `@here **Welcome to the ${clientName.value} Internal Audit Channel!**\n` + + `You can use this invite to access <#${internalChannel.id}>: \`${internalInvite.url}\`\n` + + `This invite will expire **on ** and has **${internalInvite.maxUses} max uses**`, ) } - const internalInvite = await createInvite(internalChannel) // External channel setup let externalChannel = interaction.guild.channels.cache.find( (channel) => channel.name === externalChannelName, ) as TextChannel - const externalChannelCreated = !externalChannel - if (externalChannelCreated) { + const externalChannelNeedsCreation = !externalChannel + if (externalChannelNeedsCreation) { externalChannel = await interaction.guild.channels.create({ name: externalChannelName, type: ChannelType.GuildText, @@ -247,6 +253,11 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { }) } + const externalInvite = await createInvite(externalChannel) + const externalInviteExpiry = Math.floor( + Date.now() / 1000 + externalInvite.maxAge, + ) + if (externalChannel) { await externalChannel.permissionOverwrites.create(externalRole, { ViewChannel: true, @@ -255,18 +266,20 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { ViewChannel: true, }) await externalChannel.send( - `@here **Welcome to the ${clientName.value} External Audit Channel!**`, + `@here **Welcome to the ${clientName.value} External Audit Channel!**\n` + + `You can use this invite to access <#${externalChannel.id}>: \`${externalInvite.url}\`\n` + + `This invite will expire **on ** and has **${externalInvite.maxUses} max uses**`, ) } - const externalInvite = await createInvite(externalChannel) // Final interaction response - if (internalChannelCreated || externalChannelCreated) { + if (internalChannelNeedsCreation || externalChannelNeedsCreation) { await interaction.editReply({ content: `**Defense audit setup complete for: ${clientName.value}**\n\n` + `Internal Channel: <#${internalChannel.id}> - Invite: \`${internalInvite.url}\`\n` + - `External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n\n` + + `External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n` + + `These invites will expire **on ** and have **${internalInvite.maxUses} max uses**\n\n` + `Roles created: <@&${internalRole.id}>, <@&${externalRole.id}>`, }) } else { @@ -275,7 +288,8 @@ export default async function sendInvite(discordClient: Client, robot: Robot) { `**Defense audit channels already set up for: ${clientName.value}**\n\n` + "These channels were found here:\n" + `- Internal Channel: <#${internalChannel.id}> - Invite: \`${internalInvite.url}\`\n` + - `- External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n\n` + + `- External Channel: <#${externalChannel.id}> - Invite: \`${externalInvite.url}\`\n` + + `These invites will expire **on ** and have **${internalInvite.maxUses} max uses**\n\n` + "We've updated permissions to these roles:\n" + `- Internal Role: <@&${internalRole.id}>\n` + `- External Role: <@&${externalRole.id}>`,