summaryrefslogtreecommitdiff
path: root/commands.ts
diff options
context:
space:
mode:
Diffstat (limited to 'commands.ts')
-rw-r--r--commands.ts215
1 files changed, 215 insertions, 0 deletions
diff --git a/commands.ts b/commands.ts
new file mode 100644
index 0000000..df78f60
--- /dev/null
+++ b/commands.ts
@@ -0,0 +1,215 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ ChannelType,
+ PermissionFlagsBits,
+ SlashCommandBuilder,
+ SlashCommandStringOption,
+ type ButtonInteraction,
+ type ChatInputCommandInteraction,
+} from "discord.js";
+
+export let slashCommands = [
+ {
+ slashCommand: new SlashCommandBuilder()
+ .setName("new")
+ .setDescription("Create a new CTF")
+ .addStringOption(
+ new SlashCommandStringOption()
+ .setName("name")
+ .setDescription("Name for the new CTF")
+ .setRequired(true)
+ ),
+ handler: newCtf,
+ },
+ {
+ slashCommand: new SlashCommandBuilder()
+ .setName("archive")
+ .setDescription("Make this CTF category read-only for everyone"),
+ handler: makeCtfCategoryReadonly,
+ },
+];
+
+export let buttonCommands = {
+ join: {
+ prefix: "ctf-category:",
+ handler: handleJoinCtfButton,
+ },
+} as const;
+
+async function newCtf(interaction: ChatInputCommandInteraction) {
+ if (!interaction.guild) {
+ await interaction.reply({
+ content: "This command can only be used inside a server.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ let name = interaction.options.getString("name", true);
+ await interaction.deferReply({ flags: "Ephemeral" });
+
+ let category = await interaction.guild.channels.create({
+ name,
+ type: ChannelType.GuildCategory,
+ permissionOverwrites: [
+ {
+ id: interaction.guild.roles.everyone,
+ deny: [PermissionFlagsBits.ViewChannel],
+ }
+ ],
+ });
+
+ await Promise.all([
+ await interaction.guild.channels.create({
+ name: "info",
+ type: ChannelType.GuildText,
+ parent: category.id,
+ position: 1,
+ }),
+ await interaction.guild.channels.create({
+ name: "general",
+ type: ChannelType.GuildText,
+ parent: category.id,
+ position: 2,
+ }),
+ await interaction.guild.channels.create({
+ name: "challs",
+ type: ChannelType.GuildForum,
+ parent: category.id,
+ position: 3,
+ }),
+ ]);
+
+ let commandChannel = (interaction.channelId && await interaction.guild.channels.fetch(interaction.channelId));
+
+ if (!commandChannel || !commandChannel.isTextBased())
+ throw new Error("Wth is this channel?");
+
+ await commandChannel.send({
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setCustomId(`${buttonCommands.join.prefix}${category.id}`)
+ .setLabel(`Join ${name}`)
+ .setStyle(ButtonStyle.Primary),
+ ),
+ ],
+ });
+
+ await interaction.editReply(
+ `Created ${category.name}.`,
+ );
+}
+
+async function makeCtfCategoryReadonly(interaction: ChatInputCommandInteraction) {
+ if (!interaction.guild) {
+ await interaction.reply({
+ content: "This command can only be used inside a server.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ if (!interaction.channel || interaction.channel.type !== ChannelType.GuildText
+ || !["general", "info"].includes(interaction.channel.name)
+ || !interaction.channel.parentId
+ ) {
+ await interaction.reply({
+ content: "This command can only be used in the `general` or `info` channel of a CTF category.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ let category = interaction.channel.parentId && await interaction.guild.channels.fetch(interaction.channel.parentId);
+ if (!category || category.type !== ChannelType.GuildCategory) {
+ await interaction.reply({
+ content: "This command can only be used in the `general` or `info` channel of a CTF category.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ await interaction.deferReply({ flags: "Ephemeral" });
+
+ let allChannels = await interaction.guild.channels.fetch();
+ let categoryChildren = [...allChannels.values()].filter((channel) => {
+ return channel !== null && "parentId" in channel && channel.parentId === category.id;
+ });
+
+ let infoChannel = categoryChildren.find((channel) => channel?.name === "info");
+ let generalChannel = categoryChildren.find((channel) => channel?.name === "general");
+ let challsChannel = categoryChildren.find((channel) => channel?.name === "challs");
+
+ if (
+ !infoChannel ||
+ !generalChannel ||
+ !challsChannel ||
+ infoChannel.type !== ChannelType.GuildText ||
+ generalChannel.type !== ChannelType.GuildText ||
+ challsChannel.type !== ChannelType.GuildForum
+ ) {
+ await interaction.editReply("This command can only be used in the `general` or `info` channel of a CTF category.");
+ return;
+ }
+
+ let everyoneRoleId = interaction.guild.roles.everyone.id;
+
+ await category.permissionOverwrites.edit(everyoneRoleId, {
+ ViewChannel: true,
+ ReadMessageHistory: true,
+ SendMessages: false,
+ SendMessagesInThreads: false,
+ CreatePublicThreads: false,
+ CreatePrivateThreads: false,
+ });
+
+ await Promise.all(
+ category.permissionOverwrites.cache
+ .filter(overwrite => overwrite.id !== everyoneRoleId)
+ .map(overwrite => category.permissionOverwrites.delete(overwrite.id))
+ );
+
+ for (let channel of [infoChannel, generalChannel, challsChannel]) {
+ if (channel && "lockPermissions" in channel && typeof channel.lockPermissions === "function") {
+ await channel.lockPermissions();
+ }
+ }
+
+ await interaction.editReply(
+ `Updated ${category.name}: @everyone can read, (mostly) no-one can write.`,
+ );
+}
+
+async function handleJoinCtfButton(interaction: ButtonInteraction, categoryId: string) {
+ if (!interaction.guild) {
+ await interaction.reply({
+ content: "This button only works inside a server.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ let categoryChannel = await interaction.guild.channels.fetch(categoryId);
+
+ if (!categoryChannel || categoryChannel.type !== ChannelType.GuildCategory) {
+ await interaction.reply({
+ content: "That category no longer exists.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ await categoryChannel.permissionOverwrites.edit(interaction.user.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ });
+
+ await interaction.reply({
+ content: `Granted you access to ${categoryChannel.name}.`,
+ flags: "Ephemeral",
+ });
+}
+