summaryrefslogtreecommitdiff
path: root/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'index.ts')
-rw-r--r--index.ts247
1 files changed, 235 insertions, 12 deletions
diff --git a/index.ts b/index.ts
index 913cf55..6728910 100644
--- a/index.ts
+++ b/index.ts
@@ -1,33 +1,236 @@
import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
ChannelType,
Client,
+ CommandInteraction,
Events,
GatewayIntentBits,
+ PermissionFlagsBits,
REST,
Routes,
SlashCommandBuilder,
SlashCommandStringOption,
+ type ButtonInteraction,
type ChatInputCommandInteraction,
} from "discord.js";
function requireEnv(key: string): string {
- const value = process.env[key];
+ let value = process.env[key];
if (!value) {
throw new Error(`Missing ${key} environment variable.`);
}
return value;
}
-const token = requireEnv("DISCORD_TOKEN");
-const clientId = requireEnv("DISCORD_CLIENT_ID");
-const guildId = requireEnv("DISCORD_GUILD_ID");
+let token = requireEnv("DISCORD_TOKEN");
+let clientId = requireEnv("DISCORD_CLIENT_ID");
+let guildId = requireEnv("DISCORD_GUILD_ID");
-const client = new Client({ intents: [GatewayIntentBits.Guilds] });
+let client = new Client({ intents: [GatewayIntentBits.Guilds] });
-const commands = [
+let commands = [
+ {
+ 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,
+ },
];
-const rest = new REST({ version: "10" }).setToken(token);
+let CATEGORY_BUTTON_PREFIX = "ctf-category:";
+
+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(`${CATEGORY_BUTTON_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) {
+ if (!interaction.guild) {
+ await interaction.reply({
+ content: "This button only works inside a server.",
+ flags: "Ephemeral",
+ });
+ return;
+ }
+
+ let categoryId = interaction.customId.slice(CATEGORY_BUTTON_PREFIX.length);
+
+ 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",
+ });
+}
+
+let rest = new REST({ version: "10" }).setToken(token);
async function registerSlashCommands() {
await rest.put(Routes.applicationGuildCommands(clientId, guildId), {
@@ -40,11 +243,31 @@ client.once(Events.ClientReady, (readyClient) => {
});
client.on(Events.InteractionCreate, async (interaction) => {
- if (!interaction.isChatInputCommand()) return;
- for (const command of commands) {
- if (interaction.commandName === command.slashCommand.name) {
- await command.handler(interaction);
- break;
+ try {
+ if (interaction.isChatInputCommand()) {
+ for (let command of commands) {
+ if (interaction.commandName === command.slashCommand.name) {
+ await command.handler(interaction);
+ break;
+ }
+ }
+ } else if (interaction.isButton() && interaction.customId.startsWith(CATEGORY_BUTTON_PREFIX)) {
+ await handleJoinCtfButton(interaction);
+ }
+ } catch (error) {
+ if (interaction instanceof CommandInteraction) {
+ console.error("Error while handling interaction", error);
+ if (interaction.deferred || interaction.replied) {
+ await interaction.followUp({
+ content: "Something went wrong! :(",
+ flags: "Ephemeral",
+ });
+ } else {
+ await interaction.reply({
+ content: "Something went wrong! :(",
+ flags: "Ephemeral",
+ });
+ }
}
}
});