Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defense channels and rolemapping #301

Merged
merged 17 commits into from
Jul 2, 2024
166 changes: 159 additions & 7 deletions discord-scripts/invite-management.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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-(?<client>.*)-audit$/
const INTERNAL_AUDIT_CHANNEL_REGEXP = /^int-(?<client>.*)-audit$/
const guildInvites = new Collection()
zuuring marked this conversation as resolved.
Show resolved Hide resolved

async function createInvite(
channel: TextChannel,
Expand All @@ -22,10 +24,39 @@ async function createInvite(
}
}

async function listInvites(discordClient: Client, robot: Robot): Promise<void> {
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}`,
)
})
zuuring marked this conversation as resolved.
Show resolved Hide resolved

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 () => {
await listInvites(discordClient, robot)
}, 1000)

// Check if create-invite command already exists, if not create it
const existingInviteCommand = (await application.commands.fetch()).find(
(command) => command.name === "create-invite",
Expand Down Expand Up @@ -81,13 +112,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: ${
Expand All @@ -97,6 +136,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("-")
Expand All @@ -108,9 +150,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,
Expand All @@ -120,6 +162,40 @@ export default async function sendInvite(discordClient: Client, robot: Robot) {
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}**`,
)
Expand All @@ -133,10 +209,86 @@ 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}`,
)
}
}
})

// 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<string, { uses: number }>()
const fetchedInvites = await member.guild.invites.fetch()
const newInvites = new Collection<string, number>(
fetchedInvites.map((invite) => [invite.code, invite.uses ?? 0]),
)
guildInvites.set(member.guild.id, newInvites)

robot.logger.info(
zuuring marked this conversation as resolved.
Show resolved Hide resolved
`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 =
typeof oldInvite === "object" ? oldInvite.uses : oldInvite
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,
) as TextChannel
if (channel) {
const channelTypeMatch = channel.name.match(/(ext|int)-(.*)-audit/)
const clientName = channelTypeMatch
? channelTypeMatch[2]
.replace(/-/g, " ")
.split(" ")
.map(
zuuring marked this conversation as resolved.
Show resolved Hide resolved
(word) =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(" ")
: ""

robot.logger.info(`Channel Name: ${channel.name}`)
zuuring marked this conversation as resolved.
Show resolved Hide resolved

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.`)
}
}
}
} else {
robot.logger.info("Could not find which invite was used.")
}
})
}
}
Loading