Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 321 additions & 18 deletions discord-scripts/invite-management.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,101 @@
import { Robot } from "hubot"
import { Client, TextChannel } from "discord.js"
import {
GuildMember,
Client,
TextChannel,
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
} from "discord.js"
import { DAY, MILLISECOND, WEEK } from "../lib/globals.ts"

const EXTERNAL_AUDIT_CHANNEL_REGEXP = /^ext-(?<client>.*)-audit$/

const guildInvites: { [guildId: string]: { [inviteCode: string]: number } } = {}

const employeeQuestion = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("employee-yes")
.setLabel("Yes")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId("employee-no")
.setLabel("No")
.setStyle(ButtonStyle.Danger),
)

const disciplineQuestion = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("engineering")
.setLabel("Engineering")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("design")
.setLabel("Design")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("product")
.setLabel("Product")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("marketing")
.setLabel("Marketing")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("business")
.setLabel("Business Development")
.setStyle(ButtonStyle.Primary),
)

const projectQuestion = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("mezo")
.setLabel("Mezo")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("acre")
.setLabel("Acre")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("embody")
.setLabel("Embody")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("tbtc")
.setLabel("tBTC")
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId("thesis")
.setLabel("Thesis*")
.setStyle(ButtonStyle.Primary),
)

interface Invitation {
project?: string
discipline?: string
}

const invitation: Invitation = {}

async function listInvites(discordClient: Client, robot: Robot): Promise<void> {
discordClient.guilds.cache.forEach(async (guild) => {
try {
const fetchInvites = await guild.invites.fetch()
if (fetchInvites) {
guildInvites[guild.id] ??= {}

fetchInvites.forEach((invite) => {
guildInvites[guild.id][invite.code] = invite.uses ?? 0
})
}
} catch (error) {
robot.logger.error(
`Failed to fetch invites for guild ${guild.name}: ${error}`,
)
}
})
}

async function createInvite(
channel: TextChannel,
maxAge = (1 * WEEK) / MILLISECOND,
Expand All @@ -15,6 +107,9 @@ async function createInvite(
unique: true,
})

// Update list of invites after new invite is created
await listInvites

return {
url: invite.url,
maxAge,
Expand All @@ -26,6 +121,11 @@ export default async function sendInvite(discordClient: Client, robot: Robot) {
const { application } = discordClient

if (application) {
// Grab list of guild 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 All @@ -52,26 +152,120 @@ export default async function sendInvite(discordClient: Client, robot: Robot) {
return
}

try {
const { channel } = interaction
if (channel instanceof TextChannel) {
const invite = await createInvite(channel)
if (invite) {
await interaction.reply(
`Here is your invite link: ${
invite.url
}\nThis invite expires in ${
(invite.maxAge / DAY) * MILLISECOND
} days and has a maximum of ${invite.maxUses} uses.`,
)
}
} else {
// Reply to the interaction asking if this is a thesis employee/contractor
await interaction.reply({
content: "**Is this a Thesis employee or contractor?**",
components: [employeeQuestion],
ephemeral: true,
})
})

discordClient.on("interactionCreate", async (interaction) => {
if (!interaction.isButton()) return

if (!interaction.guild) {
await interaction.reply(
"This interaction can only be used in a server.",
)
return
}

const { channel } = interaction
if (!(channel instanceof TextChannel)) {
await interaction.reply(
"Cannot create an invite for this type of channel.",
)
return
}
// generate an invite for base role
if (interaction.customId === "employee-no") {
try {
invitation.project = "Thesis Base"
const matchChannel = interaction.guild.channels.cache.find(
(c) => c.name === "🔒thesis-base",
) as TextChannel
robot.logger.info(matchChannel)
const invite = await createInvite(
matchChannel,
(1 * WEEK) / MILLISECOND,
2,
)
const internalInviteExpiry = Math.floor(
Date.now() / 1000 + invite.maxAge,
)
await interaction.update({
content: `**We've generated an invite code for @${invitation.project}} role**, : ${invite.url}\nThis invite expires <t:${internalInviteExpiry}:R> and has a maximum of ${invite.maxUses} uses.`,
components: [],
})
} catch (error) {
robot.logger.error(error)
await interaction.reply(
"An error occurred while creating the invite.",
)
}
}
if (interaction.customId === "employee-yes") {
await interaction.update({
content:
"**For a Thesis employee: which discipline(s) will this person be working with?**",
components: [disciplineQuestion],
})
invitation.discipline = await interaction.customId
}

if (
interaction.customId === "engineering" ||
interaction.customId === "design" ||
interaction.customId === "product" ||
interaction.customId === "marketing" ||
interaction.customId === "business"
) {
invitation.discipline = await interaction.customId
robot.logger.info(invitation.discipline)
await interaction.update({
content:
"**For a Thesis employee: which projects will this person be working on?**",
components: [projectQuestion],
})
}

if (
interaction.customId === "mezo" ||
interaction.customId === "acre" ||
interaction.customId === "embody" ||
interaction.customId === "tbtc" ||
interaction.customId === "thesis"
) {
invitation.project = await interaction.customId
robot.logger.info(invitation.project)
robot.logger.info(invitation.project, invitation.discipline)
const targetChannelName = `🔒${invitation.project ?? ""}-${
invitation.discipline ?? ""
}`
robot.logger.info(targetChannelName)
const matchChannel = interaction.guild.channels.cache.find(
(c) => c.name === targetChannelName,
) as TextChannel
robot.logger.info(matchChannel)
try {
const invite = await createInvite(
matchChannel,
(1 * WEEK) / MILLISECOND,
2,
)
const internalInviteExpiry = Math.floor(
Date.now() / 1000 + invite.maxAge,
)
await interaction.update({
content: `**We've generated an invite code for <@${interaction.customId}> role**, : ${invite.url}\nThis invite expires <t:${internalInviteExpiry}:R> and has a maximum of ${invite.maxUses} uses.`,
components: [],
})
} catch (error) {
robot.logger.error(error)
await interaction.reply(
"Cannot create an invite for this type of channel.",
"An error occurred while creating the invite.",
)
}
} catch (error) {
await interaction.reply("An error occurred while creating the invite.")
}
})

Expand Down Expand Up @@ -138,5 +332,114 @@ 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[member.guild.id] || {}
const fetchedInvites = await member.guild.invites.fetch()

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 oldUses = oldInvites[fetchedInvite.code] || 0
return (fetchedInvite.uses ?? 0) > oldUses
})

robot.logger.info("Old invites:", oldInvites)
robot.logger.info("new invites:", newInvites)
if (usedInvite && usedInvite.channelId) {
const channel = member.guild.channels.cache.get(
usedInvite.channelId,
) as TextChannel
if (channel) {
robot.logger.info(channel)
const auditChannelMatch = channel.name.match(/(ext|int)-(.*)-audit/)
if (auditChannelMatch) {
const clientName = auditChannelMatch
? auditChannelMatch[2]
.replace(/-/g, " ")
.split(" ")
.map(
(word) =>
word.charAt(0).toUpperCase() +
word.slice(1).toLowerCase(),
)
.join(" ")
: ""
const auditType =
auditChannelMatch[1] === "ext" ? "External" : "Internal"
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(
`Invite code used: ${
usedInvite ? usedInvite.code : "None"
}, Username joined: ${
member.displayName
}, Role assignments: ${roleName}`,
)
}

if (!auditChannelMatch) {
const cleanChannelName = channel.name.replace(/🔒/g, "").trim()
const rolesToAssign = cleanChannelName.split("-")

if (rolesToAssign.includes("base")) {
robot.logger.info("Thesis base role detected")
const baseRole = member.guild.roles.cache.find(
(r) => r.name === "Thesis Base",
)
if (baseRole) {
await member.roles.add(baseRole)
}
robot.logger.info(
`Invite code used: ${
usedInvite ? usedInvite.code : "None"
}, Username joined: ${
member.displayName
}, Role assignment: ${baseRole}`,
)
}

if (rolesToAssign.length >= 2 && !rolesToAssign.includes("base")) {
const role1Name = rolesToAssign[0].trim()
const role2Name = rolesToAssign[1].trim()
const role1 = member.guild.roles.cache.find(
(r) => r.name.toLowerCase() === role1Name.toLowerCase(),
)
if (role1) {
await member.roles.add(role1)
}

const role2 = member.guild.roles.cache.find(
(r) => r.name.toLowerCase() === role2Name.toLowerCase(),
)
if (role2) {
await member.roles.add(role2)
}
robot.logger.info(
`Invite code used: ${
usedInvite ? usedInvite.code : "None"
}, Username joined: ${
member.displayName
}, Role assignments: ${role1} ${role2}`,
)
}
}
}
} else {
robot.logger.info("Could not find which invite was used.")
}
})
}
}