From 653294ecf3e1c946d7c2b5f6c57641fc19e80830 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 10 Apr 2024 11:15:55 +0300 Subject: [PATCH 1/8] Channel Archiver ### Notes This WIP PR focuses on building a channel archiver function into Valkyrie. So that you can easily dump channel logs from a specific channel at point of archive. Due to Discord APIs message rate limit of 100 messages, we run a loop depending on the total number of messages in the channel, then combine into a file which is sent back to the channel. ### WIP This is just the initial test setup of this, some things we need to add still are: - Better storage for the archival message file (dotenv is temp) - Message headers specifying date of archive, channel name, etc - Permission flags, access to archive - Introduction of a command, valkyrie interface, etc --- discord-scripts/channel-management.ts | 80 +++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 5 ++ 3 files changed, 86 insertions(+) create mode 100644 discord-scripts/channel-management.ts diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts new file mode 100644 index 00000000..402e713b --- /dev/null +++ b/discord-scripts/channel-management.ts @@ -0,0 +1,80 @@ +import { Robot } from "hubot" +import { + Client, + AttachmentBuilder, + TextChannel, + Message, + Collection, +} from "discord.js" +import { writeFile, unlink } from "fs/promises" +import dotenv from "dotenv" + +dotenv.config() + +async function fetchAllMessages( + channel: TextChannel, + before?: string, +): Promise>> { + const limit = 100 + const options = before ? { limit, before } : { limit } + const fetchedMessages = await channel.messages.fetch(options) + + if (fetchedMessages.size === 0) { + return new Collection>() + } + + const lastId = fetchedMessages.lastKey() + const olderMessages = await fetchAllMessages(channel, lastId) + + return new Collection>().concat( + fetchedMessages, + olderMessages, + ) +} + +export default async function archiveChannel( + discordClient: Client, + robot: Robot, +) { + const { application } = discordClient + + if (application) { + // Dump all messages from a channel into an array after "!archive" + discordClient.on("messageCreate", async (message) => { + if ( + message.content.toLowerCase() === "!archive" && + message.channel instanceof TextChannel + ) { + try { + const allMessages = await fetchAllMessages(message.channel) + + robot.logger.info(`Total messages fetched: ${allMessages.size}`) + + const messagesArray = Array.from(allMessages.values()).reverse() + const messagesContent = messagesArray + .map( + (m) => + `${m.createdAt.toLocaleString()}: ${m.author.username}: ${ + m.content + }`, + ) + .join("\n") + + const filePath = "./messages.txt" + await writeFile(filePath, messagesContent, "utf-8") + + const fileAttachment = new AttachmentBuilder(filePath) + await message.channel + .send({ + content: "Here are the archived messages:", + files: [fileAttachment], + }) + .then(() => unlink(filePath)) + .catch(robot.logger.error) + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) + } + } + }) + } +} diff --git a/package.json b/package.json index 7c780e4e..db720d4c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "cronstrue": "^1.68.0", "decode-html": "^2.0.0", "discord.js": "^14.8.0", + "dotenv": "^16.4.5", "express": "^4.18.2", "figma-api": "^1.11.0", "github-api": "^3.4.0", diff --git a/yarn.lock b/yarn.lock index dbe8051e..8bdfd4ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" From 0867f83be953604dd2c95fede11f5babe0ed4652 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 19 Apr 2024 12:12:31 +0300 Subject: [PATCH 2/8] Add archival category Adds the feature so that when the archival command is run (!archive placeholder for now), move and lock the channel into a `archived-channels` category. Still WIP. Note: Needs extended permission flags for bot to be enabled `Administrator, Manage Channels, Manage Guild` --- discord-scripts/channel-management.ts | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 402e713b..317131cb 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -3,6 +3,7 @@ import { Client, AttachmentBuilder, TextChannel, + ChannelType, Message, Collection, } from "discord.js" @@ -39,7 +40,7 @@ export default async function archiveChannel( const { application } = discordClient if (application) { - // Dump all messages from a channel into an array after "!archive" + // Dump all messages into a text file and then move channel to it's own category "archived-messages" discordClient.on("messageCreate", async (message) => { if ( message.content.toLowerCase() === "!archive" && @@ -71,6 +72,40 @@ export default async function archiveChannel( }) .then(() => unlink(filePath)) .catch(robot.logger.error) + + if (!message.guild) { + robot.logger.error( + "This command cannot be executed outside of a guild.", + ) + return + } + + let archivedCategory = discordClient.channels.cache.find( + (c) => + c.type === ChannelType.GuildCategory && + c.name.toLowerCase() === "archived-channels", + ) + if (!archivedCategory) { + if (message.guild && archivedCategory) { + archivedCategory = await message.guild.channels.create({ + name: "archived-channels", + type: ChannelType.GuildCategory, + }) + await message.channel.setParent(archivedCategory.id) + await message.channel.send("Channel archived") + } + } + + if (archivedCategory) { + await message.channel.setParent(archivedCategory.id) + await message.channel.permissionOverwrites.edit(message.guild.id, { + SendMessages: false, + }) + await message.channel.send( + "Channel archived, locked and moved to archived channel category", + ) + robot.logger.info("Channel archived and locked successfully.") + } } catch (error) { robot.logger.error(`An error occurred: ${error}`) } From efa640b91f92b0ea85a3bacff86630fb6f0cf91c Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 19 Apr 2024 16:11:02 +0300 Subject: [PATCH 3/8] Add unarchive logging Just for viewing the audit logs and what's going on for channelUpdate events in the discord audit logs. --- discord-scripts/channel-management.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 317131cb..79d4f757 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -1,6 +1,7 @@ import { Robot } from "hubot" import { Client, + AuditLogEvent, AttachmentBuilder, TextChannel, ChannelType, @@ -111,5 +112,36 @@ export default async function archiveChannel( } } }) + + // WIP, just for debugging in order to track auditlog events, update: it does not seem as though parent.id changes are stored + discordClient.on("messageCreate", async (message) => { + if ( + message.content.toLowerCase() === "!unarchive" && + message.channel instanceof TextChannel && + message.guild + ) { + try { + const logs = await message.guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelUpdate, + limit: 100, + }) + const latestEntries = Array.from(logs.entries.values()).slice(0, 50) + latestEntries.forEach((entry) => { + if (entry.changes) { + entry.changes.forEach((change) => { + robot.logger.info( + `Change Key: ${change.key}, Old Value: ${change.old}, New Value: ${change.new}`, + ) + }) + } + }) + } catch (error) { + robot.logger.error( + `An error occurred while trying to unarchive the channel: ${error}`, + ) + message.channel.send("Failed to unarchive the channel.") + } + } + }) } } From 97ce59fb7a14a86b3dcb6159fa8154ff74122581 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Thu, 25 Apr 2024 13:01:20 +0300 Subject: [PATCH 4/8] Remove file storage This commit removes the file transcript on the archiver script and also setups up the proper /unarchive /archive commands. --- discord-scripts/channel-management.ts | 221 +++++++++++++------------- 1 file changed, 109 insertions(+), 112 deletions(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 79d4f757..1a0f2932 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -1,38 +1,5 @@ import { Robot } from "hubot" -import { - Client, - AuditLogEvent, - AttachmentBuilder, - TextChannel, - ChannelType, - Message, - Collection, -} from "discord.js" -import { writeFile, unlink } from "fs/promises" -import dotenv from "dotenv" - -dotenv.config() - -async function fetchAllMessages( - channel: TextChannel, - before?: string, -): Promise>> { - const limit = 100 - const options = before ? { limit, before } : { limit } - const fetchedMessages = await channel.messages.fetch(options) - - if (fetchedMessages.size === 0) { - return new Collection>() - } - - const lastId = fetchedMessages.lastKey() - const olderMessages = await fetchAllMessages(channel, lastId) - - return new Collection>().concat( - fetchedMessages, - olderMessages, - ) -} +import { Client, TextChannel, ChannelType } from "discord.js" export default async function archiveChannel( discordClient: Client, @@ -41,106 +8,136 @@ export default async function archiveChannel( const { application } = discordClient if (application) { - // Dump all messages into a text file and then move channel to it's own category "archived-messages" - discordClient.on("messageCreate", async (message) => { - if ( - message.content.toLowerCase() === "!archive" && - message.channel instanceof TextChannel - ) { - try { - const allMessages = await fetchAllMessages(message.channel) + // Check if archive-channel command already exists, if not create it + const existingArchiveCommand = (await application.commands.fetch()).find( + (command) => command.name === "archive-channel", + ) + if (existingArchiveCommand === undefined) { + robot.logger.info("No archive-channel command found, creating it!") + await application.commands.create({ + name: "archive-channel", + description: + "Archives channel to archived channels category (Defense only)", + }) + robot.logger.info("archive channel command set") + } - robot.logger.info(`Total messages fetched: ${allMessages.size}`) + // Check if unarchive-channel command already exists, if not create it + const existingUnarchiveCommand = (await application.commands.fetch()).find( + (command) => command.name === "unarchive-channel", + ) + if (existingUnarchiveCommand === undefined) { + robot.logger.info("No unarchive-channel command found, creating it!") + await application.commands.create({ + name: "unarchive-channel", + description: + "unarchive channel back to defense category (Defense only)", + }) + robot.logger.info("unarchive channel command set") + } - const messagesArray = Array.from(allMessages.values()).reverse() - const messagesContent = messagesArray - .map( - (m) => - `${m.createdAt.toLocaleString()}: ${m.author.username}: ${ - m.content - }`, - ) - .join("\n") - - const filePath = "./messages.txt" - await writeFile(filePath, messagesContent, "utf-8") - - const fileAttachment = new AttachmentBuilder(filePath) - await message.channel - .send({ - content: "Here are the archived messages:", - files: [fileAttachment], - }) - .then(() => unlink(filePath)) - .catch(robot.logger.error) - - if (!message.guild) { - robot.logger.error( - "This command cannot be executed outside of a guild.", - ) - return - } - - let archivedCategory = discordClient.channels.cache.find( + // Move channel to archived-channel category + discordClient.on("interactionCreate", async (interaction) => { + if ( + !interaction.isCommand() || + interaction.commandName !== "archive-channel" + ) { + return + } + if (!interaction.guild) { + await interaction.reply("This command can only be used in a server.") + return + } + try { + if (interaction.channel instanceof TextChannel) { + let archivedCategory = interaction.guild.channels.cache.find( (c) => c.type === ChannelType.GuildCategory && c.name.toLowerCase() === "archived-channels", ) + if (!archivedCategory) { - if (message.guild && archivedCategory) { - archivedCategory = await message.guild.channels.create({ - name: "archived-channels", - type: ChannelType.GuildCategory, - }) - await message.channel.setParent(archivedCategory.id) - await message.channel.send("Channel archived") - } + archivedCategory = await interaction.guild.channels.create({ + name: "archived-channels", + type: ChannelType.GuildCategory, + }) } if (archivedCategory) { - await message.channel.setParent(archivedCategory.id) - await message.channel.permissionOverwrites.edit(message.guild.id, { - SendMessages: false, - }) - await message.channel.send( + ;(await interaction.channel.setParent( + archivedCategory.id, + )) as TextChannel + await interaction.channel.permissionOverwrites.edit( + interaction.guild.id, + { + SendMessages: false, + }, + ) + await interaction.channel.send( "Channel archived, locked and moved to archived channel category", ) robot.logger.info("Channel archived and locked successfully.") } - } catch (error) { - robot.logger.error(`An error occurred: ${error}`) } + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) + await interaction.reply( + `An error occurred: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ) } }) - // WIP, just for debugging in order to track auditlog events, update: it does not seem as though parent.id changes are stored - discordClient.on("messageCreate", async (message) => { + // Move channel back to defense category on Unarchived + discordClient.on("interactionCreate", async (interaction) => { if ( - message.content.toLowerCase() === "!unarchive" && - message.channel instanceof TextChannel && - message.guild + !interaction.isCommand() || + interaction.commandName !== "unarchive-channel" ) { - try { - const logs = await message.guild.fetchAuditLogs({ - type: AuditLogEvent.ChannelUpdate, - limit: 100, - }) - const latestEntries = Array.from(logs.entries.values()).slice(0, 50) - latestEntries.forEach((entry) => { - if (entry.changes) { - entry.changes.forEach((change) => { - robot.logger.info( - `Change Key: ${change.key}, Old Value: ${change.old}, New Value: ${change.new}`, - ) - }) - } - }) - } catch (error) { - robot.logger.error( - `An error occurred while trying to unarchive the channel: ${error}`, + return + } + if (!interaction.guild) { + await interaction.reply("This command can only be used in a server.") + return + } + try { + if (interaction.channel instanceof TextChannel) { + const defenseCategory = interaction.guild.channels.cache.find( + (c) => + c.type === ChannelType.GuildCategory && + c.name.toLowerCase() === "defense", ) - message.channel.send("Failed to unarchive the channel.") + + if (!defenseCategory) { + await interaction.reply( + "No defense category found to move channel to", + ) + } + + if (defenseCategory) { + ;(await interaction.channel.setParent( + defenseCategory.id, + )) as TextChannel + await interaction.channel.permissionOverwrites.edit( + interaction.guild.id, + { + SendMessages: false, + }, + ) + await interaction.channel.send( + "Channel unarchived and move backed to defense category", + ) + robot.logger.info("Channel uarchived and moved.") + } } + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) + await interaction.reply( + `An error occurred: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ) } }) } From edef2530b9dd887d462a01cabf4bd385b18b4053 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 10 May 2024 11:13:00 +0300 Subject: [PATCH 5/8] Revert "Remove file storage" This reverts commit 97ce59fb7a14a86b3dcb6159fa8154ff74122581. --- discord-scripts/channel-management.ts | 221 +++++++++++++------------- 1 file changed, 112 insertions(+), 109 deletions(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 1a0f2932..79d4f757 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -1,5 +1,38 @@ import { Robot } from "hubot" -import { Client, TextChannel, ChannelType } from "discord.js" +import { + Client, + AuditLogEvent, + AttachmentBuilder, + TextChannel, + ChannelType, + Message, + Collection, +} from "discord.js" +import { writeFile, unlink } from "fs/promises" +import dotenv from "dotenv" + +dotenv.config() + +async function fetchAllMessages( + channel: TextChannel, + before?: string, +): Promise>> { + const limit = 100 + const options = before ? { limit, before } : { limit } + const fetchedMessages = await channel.messages.fetch(options) + + if (fetchedMessages.size === 0) { + return new Collection>() + } + + const lastId = fetchedMessages.lastKey() + const olderMessages = await fetchAllMessages(channel, lastId) + + return new Collection>().concat( + fetchedMessages, + olderMessages, + ) +} export default async function archiveChannel( discordClient: Client, @@ -8,136 +41,106 @@ export default async function archiveChannel( const { application } = discordClient if (application) { - // Check if archive-channel command already exists, if not create it - const existingArchiveCommand = (await application.commands.fetch()).find( - (command) => command.name === "archive-channel", - ) - if (existingArchiveCommand === undefined) { - robot.logger.info("No archive-channel command found, creating it!") - await application.commands.create({ - name: "archive-channel", - description: - "Archives channel to archived channels category (Defense only)", - }) - robot.logger.info("archive channel command set") - } - - // Check if unarchive-channel command already exists, if not create it - const existingUnarchiveCommand = (await application.commands.fetch()).find( - (command) => command.name === "unarchive-channel", - ) - if (existingUnarchiveCommand === undefined) { - robot.logger.info("No unarchive-channel command found, creating it!") - await application.commands.create({ - name: "unarchive-channel", - description: - "unarchive channel back to defense category (Defense only)", - }) - robot.logger.info("unarchive channel command set") - } - - // Move channel to archived-channel category - discordClient.on("interactionCreate", async (interaction) => { + // Dump all messages into a text file and then move channel to it's own category "archived-messages" + discordClient.on("messageCreate", async (message) => { if ( - !interaction.isCommand() || - interaction.commandName !== "archive-channel" + message.content.toLowerCase() === "!archive" && + message.channel instanceof TextChannel ) { - return - } - if (!interaction.guild) { - await interaction.reply("This command can only be used in a server.") - return - } - try { - if (interaction.channel instanceof TextChannel) { - let archivedCategory = interaction.guild.channels.cache.find( + try { + const allMessages = await fetchAllMessages(message.channel) + + robot.logger.info(`Total messages fetched: ${allMessages.size}`) + + const messagesArray = Array.from(allMessages.values()).reverse() + const messagesContent = messagesArray + .map( + (m) => + `${m.createdAt.toLocaleString()}: ${m.author.username}: ${ + m.content + }`, + ) + .join("\n") + + const filePath = "./messages.txt" + await writeFile(filePath, messagesContent, "utf-8") + + const fileAttachment = new AttachmentBuilder(filePath) + await message.channel + .send({ + content: "Here are the archived messages:", + files: [fileAttachment], + }) + .then(() => unlink(filePath)) + .catch(robot.logger.error) + + if (!message.guild) { + robot.logger.error( + "This command cannot be executed outside of a guild.", + ) + return + } + + let archivedCategory = discordClient.channels.cache.find( (c) => c.type === ChannelType.GuildCategory && c.name.toLowerCase() === "archived-channels", ) - if (!archivedCategory) { - archivedCategory = await interaction.guild.channels.create({ - name: "archived-channels", - type: ChannelType.GuildCategory, - }) + if (message.guild && archivedCategory) { + archivedCategory = await message.guild.channels.create({ + name: "archived-channels", + type: ChannelType.GuildCategory, + }) + await message.channel.setParent(archivedCategory.id) + await message.channel.send("Channel archived") + } } if (archivedCategory) { - ;(await interaction.channel.setParent( - archivedCategory.id, - )) as TextChannel - await interaction.channel.permissionOverwrites.edit( - interaction.guild.id, - { - SendMessages: false, - }, - ) - await interaction.channel.send( + await message.channel.setParent(archivedCategory.id) + await message.channel.permissionOverwrites.edit(message.guild.id, { + SendMessages: false, + }) + await message.channel.send( "Channel archived, locked and moved to archived channel category", ) robot.logger.info("Channel archived and locked successfully.") } + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) } - } catch (error) { - robot.logger.error(`An error occurred: ${error}`) - await interaction.reply( - `An error occurred: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ) } }) - // Move channel back to defense category on Unarchived - discordClient.on("interactionCreate", async (interaction) => { + // WIP, just for debugging in order to track auditlog events, update: it does not seem as though parent.id changes are stored + discordClient.on("messageCreate", async (message) => { if ( - !interaction.isCommand() || - interaction.commandName !== "unarchive-channel" + message.content.toLowerCase() === "!unarchive" && + message.channel instanceof TextChannel && + message.guild ) { - return - } - if (!interaction.guild) { - await interaction.reply("This command can only be used in a server.") - return - } - try { - if (interaction.channel instanceof TextChannel) { - const defenseCategory = interaction.guild.channels.cache.find( - (c) => - c.type === ChannelType.GuildCategory && - c.name.toLowerCase() === "defense", + try { + const logs = await message.guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelUpdate, + limit: 100, + }) + const latestEntries = Array.from(logs.entries.values()).slice(0, 50) + latestEntries.forEach((entry) => { + if (entry.changes) { + entry.changes.forEach((change) => { + robot.logger.info( + `Change Key: ${change.key}, Old Value: ${change.old}, New Value: ${change.new}`, + ) + }) + } + }) + } catch (error) { + robot.logger.error( + `An error occurred while trying to unarchive the channel: ${error}`, ) - - if (!defenseCategory) { - await interaction.reply( - "No defense category found to move channel to", - ) - } - - if (defenseCategory) { - ;(await interaction.channel.setParent( - defenseCategory.id, - )) as TextChannel - await interaction.channel.permissionOverwrites.edit( - interaction.guild.id, - { - SendMessages: false, - }, - ) - await interaction.channel.send( - "Channel unarchived and move backed to defense category", - ) - robot.logger.info("Channel uarchived and moved.") - } + message.channel.send("Failed to unarchive the channel.") } - } catch (error) { - robot.logger.error(`An error occurred: ${error}`) - await interaction.reply( - `An error occurred: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ) } }) } From 2812e1bbcb1876715f222f519769147de2c7f89f Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 15 May 2024 10:49:35 +0300 Subject: [PATCH 6/8] Reapply "Remove file storage" This reverts commit edef2530b9dd887d462a01cabf4bd385b18b4053. --- discord-scripts/channel-management.ts | 221 +++++++++++++------------- 1 file changed, 109 insertions(+), 112 deletions(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 79d4f757..1a0f2932 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -1,38 +1,5 @@ import { Robot } from "hubot" -import { - Client, - AuditLogEvent, - AttachmentBuilder, - TextChannel, - ChannelType, - Message, - Collection, -} from "discord.js" -import { writeFile, unlink } from "fs/promises" -import dotenv from "dotenv" - -dotenv.config() - -async function fetchAllMessages( - channel: TextChannel, - before?: string, -): Promise>> { - const limit = 100 - const options = before ? { limit, before } : { limit } - const fetchedMessages = await channel.messages.fetch(options) - - if (fetchedMessages.size === 0) { - return new Collection>() - } - - const lastId = fetchedMessages.lastKey() - const olderMessages = await fetchAllMessages(channel, lastId) - - return new Collection>().concat( - fetchedMessages, - olderMessages, - ) -} +import { Client, TextChannel, ChannelType } from "discord.js" export default async function archiveChannel( discordClient: Client, @@ -41,106 +8,136 @@ export default async function archiveChannel( const { application } = discordClient if (application) { - // Dump all messages into a text file and then move channel to it's own category "archived-messages" - discordClient.on("messageCreate", async (message) => { - if ( - message.content.toLowerCase() === "!archive" && - message.channel instanceof TextChannel - ) { - try { - const allMessages = await fetchAllMessages(message.channel) + // Check if archive-channel command already exists, if not create it + const existingArchiveCommand = (await application.commands.fetch()).find( + (command) => command.name === "archive-channel", + ) + if (existingArchiveCommand === undefined) { + robot.logger.info("No archive-channel command found, creating it!") + await application.commands.create({ + name: "archive-channel", + description: + "Archives channel to archived channels category (Defense only)", + }) + robot.logger.info("archive channel command set") + } - robot.logger.info(`Total messages fetched: ${allMessages.size}`) + // Check if unarchive-channel command already exists, if not create it + const existingUnarchiveCommand = (await application.commands.fetch()).find( + (command) => command.name === "unarchive-channel", + ) + if (existingUnarchiveCommand === undefined) { + robot.logger.info("No unarchive-channel command found, creating it!") + await application.commands.create({ + name: "unarchive-channel", + description: + "unarchive channel back to defense category (Defense only)", + }) + robot.logger.info("unarchive channel command set") + } - const messagesArray = Array.from(allMessages.values()).reverse() - const messagesContent = messagesArray - .map( - (m) => - `${m.createdAt.toLocaleString()}: ${m.author.username}: ${ - m.content - }`, - ) - .join("\n") - - const filePath = "./messages.txt" - await writeFile(filePath, messagesContent, "utf-8") - - const fileAttachment = new AttachmentBuilder(filePath) - await message.channel - .send({ - content: "Here are the archived messages:", - files: [fileAttachment], - }) - .then(() => unlink(filePath)) - .catch(robot.logger.error) - - if (!message.guild) { - robot.logger.error( - "This command cannot be executed outside of a guild.", - ) - return - } - - let archivedCategory = discordClient.channels.cache.find( + // Move channel to archived-channel category + discordClient.on("interactionCreate", async (interaction) => { + if ( + !interaction.isCommand() || + interaction.commandName !== "archive-channel" + ) { + return + } + if (!interaction.guild) { + await interaction.reply("This command can only be used in a server.") + return + } + try { + if (interaction.channel instanceof TextChannel) { + let archivedCategory = interaction.guild.channels.cache.find( (c) => c.type === ChannelType.GuildCategory && c.name.toLowerCase() === "archived-channels", ) + if (!archivedCategory) { - if (message.guild && archivedCategory) { - archivedCategory = await message.guild.channels.create({ - name: "archived-channels", - type: ChannelType.GuildCategory, - }) - await message.channel.setParent(archivedCategory.id) - await message.channel.send("Channel archived") - } + archivedCategory = await interaction.guild.channels.create({ + name: "archived-channels", + type: ChannelType.GuildCategory, + }) } if (archivedCategory) { - await message.channel.setParent(archivedCategory.id) - await message.channel.permissionOverwrites.edit(message.guild.id, { - SendMessages: false, - }) - await message.channel.send( + ;(await interaction.channel.setParent( + archivedCategory.id, + )) as TextChannel + await interaction.channel.permissionOverwrites.edit( + interaction.guild.id, + { + SendMessages: false, + }, + ) + await interaction.channel.send( "Channel archived, locked and moved to archived channel category", ) robot.logger.info("Channel archived and locked successfully.") } - } catch (error) { - robot.logger.error(`An error occurred: ${error}`) } + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) + await interaction.reply( + `An error occurred: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ) } }) - // WIP, just for debugging in order to track auditlog events, update: it does not seem as though parent.id changes are stored - discordClient.on("messageCreate", async (message) => { + // Move channel back to defense category on Unarchived + discordClient.on("interactionCreate", async (interaction) => { if ( - message.content.toLowerCase() === "!unarchive" && - message.channel instanceof TextChannel && - message.guild + !interaction.isCommand() || + interaction.commandName !== "unarchive-channel" ) { - try { - const logs = await message.guild.fetchAuditLogs({ - type: AuditLogEvent.ChannelUpdate, - limit: 100, - }) - const latestEntries = Array.from(logs.entries.values()).slice(0, 50) - latestEntries.forEach((entry) => { - if (entry.changes) { - entry.changes.forEach((change) => { - robot.logger.info( - `Change Key: ${change.key}, Old Value: ${change.old}, New Value: ${change.new}`, - ) - }) - } - }) - } catch (error) { - robot.logger.error( - `An error occurred while trying to unarchive the channel: ${error}`, + return + } + if (!interaction.guild) { + await interaction.reply("This command can only be used in a server.") + return + } + try { + if (interaction.channel instanceof TextChannel) { + const defenseCategory = interaction.guild.channels.cache.find( + (c) => + c.type === ChannelType.GuildCategory && + c.name.toLowerCase() === "defense", ) - message.channel.send("Failed to unarchive the channel.") + + if (!defenseCategory) { + await interaction.reply( + "No defense category found to move channel to", + ) + } + + if (defenseCategory) { + ;(await interaction.channel.setParent( + defenseCategory.id, + )) as TextChannel + await interaction.channel.permissionOverwrites.edit( + interaction.guild.id, + { + SendMessages: false, + }, + ) + await interaction.channel.send( + "Channel unarchived and move backed to defense category", + ) + robot.logger.info("Channel uarchived and moved.") + } } + } catch (error) { + robot.logger.error(`An error occurred: ${error}`) + await interaction.reply( + `An error occurred: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ) } }) } From d3f0577fe1442ff3248ae9ac8e1e5f11ed570301 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 15 May 2024 11:36:07 +0300 Subject: [PATCH 7/8] Refactoring archival flow --- discord-scripts/channel-management.ts | 145 +++++++++++++++++--------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 1a0f2932..9bd21939 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -1,5 +1,35 @@ import { Robot } from "hubot" -import { Client, TextChannel, ChannelType } from "discord.js" +import { + Client, + TextChannel, + ChannelType, + AttachmentBuilder, + Collection, + Message, +} from "discord.js" +import { writeFile, unlink } from "fs/promises" + +// fetch messages in batch of 100 in-order to go past rate limit. +async function fetchAllMessages( + channel: TextChannel, + before?: string, +): Promise>> { + const limit = 100 + const options = before ? { limit, before } : { limit } + const fetchedMessages = await channel.messages.fetch(options) + + if (fetchedMessages.size === 0) { + return new Collection>() + } + + const lastId = fetchedMessages.lastKey() + const olderMessages = await fetchAllMessages(channel, lastId) + + return new Collection>().concat( + fetchedMessages, + olderMessages, + ) +} export default async function archiveChannel( discordClient: Client, @@ -8,7 +38,7 @@ export default async function archiveChannel( const { application } = discordClient if (application) { - // Check if archive-channel command already exists, if not create it + // check if archive-channel command already exists, if not create it const existingArchiveCommand = (await application.commands.fetch()).find( (command) => command.name === "archive-channel", ) @@ -22,7 +52,7 @@ export default async function archiveChannel( robot.logger.info("archive channel command set") } - // Check if unarchive-channel command already exists, if not create it + // check if unarchive-channel command already exists, if not create it const existingUnarchiveCommand = (await application.commands.fetch()).find( (command) => command.name === "unarchive-channel", ) @@ -36,7 +66,7 @@ export default async function archiveChannel( robot.logger.info("unarchive channel command set") } - // Move channel to archived-channel category + // move channel to archived-channel category and send out transcript to interaction discordClient.on("interactionCreate", async (interaction) => { if ( !interaction.isCommand() || @@ -44,52 +74,73 @@ export default async function archiveChannel( ) { return } - if (!interaction.guild) { - await interaction.reply("This command can only be used in a server.") + if (!interaction.guild || !(interaction.channel instanceof TextChannel)) { return } + try { - if (interaction.channel instanceof TextChannel) { - let archivedCategory = interaction.guild.channels.cache.find( - (c) => - c.type === ChannelType.GuildCategory && - c.name.toLowerCase() === "archived-channels", + const allMessages = await fetchAllMessages(interaction.channel) + robot.logger.info(`Total messages fetched: ${allMessages}`) + + const messagesContent = allMessages + .reverse() + .map( + (m) => + `${m.createdAt.toLocaleString()}: ${m.author.username}: ${ + m.content + }`, ) + .join("\n") - if (!archivedCategory) { - archivedCategory = await interaction.guild.channels.create({ - name: "archived-channels", - type: ChannelType.GuildCategory, - }) - } + const filePath = `${interaction.channel.name}_transcript.txt` + await writeFile(filePath, messagesContent, "utf-8") - if (archivedCategory) { - ;(await interaction.channel.setParent( - archivedCategory.id, - )) as TextChannel - await interaction.channel.permissionOverwrites.edit( - interaction.guild.id, - { - SendMessages: false, - }, - ) - await interaction.channel.send( - "Channel archived, locked and moved to archived channel category", + // check for or create archived category + let archivedCategory = interaction.guild.channels.cache.find( + (c) => + c.type === ChannelType.GuildCategory && + c.name.toLowerCase() === "archived-channels", + ) + if (!archivedCategory) { + archivedCategory = await interaction.guild.channels.create({ + name: "archived-channels", + type: ChannelType.GuildCategory, + }) + } + + // move channel and set permissions + if (archivedCategory) { + await interaction.channel.setParent(archivedCategory.id) + await interaction.channel.permissionOverwrites.edit( + interaction.guild.id, + { SendMessages: false }, + ) + await interaction.reply( + "**Channel archived, locked and moved to #archived-channel category**", + ) + + // upload chat transcript to channel and then delete it + const fileAttachment = new AttachmentBuilder(filePath) + await interaction.channel + .send({ + content: "**Here is a transcript of the channel messages:**", + files: [fileAttachment], + }) + .then(() => unlink(filePath)) + .catch((error) => + robot.logger.error(`Failed to delete file: ${error}`), ) - robot.logger.info("Channel archived and locked successfully.") - } + + robot.logger.info( + "Channel archived and locked successfully, messages saved.", + ) } } catch (error) { robot.logger.error(`An error occurred: ${error}`) - await interaction.reply( - `An error occurred: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ) } }) - // Move channel back to defense category on Unarchived + // move channel back to defense category on Unarchived discordClient.on("interactionCreate", async (interaction) => { if ( !interaction.isCommand() || @@ -98,7 +149,6 @@ export default async function archiveChannel( return } if (!interaction.guild) { - await interaction.reply("This command can only be used in a server.") return } try { @@ -116,28 +166,19 @@ export default async function archiveChannel( } if (defenseCategory) { - ;(await interaction.channel.setParent( - defenseCategory.id, - )) as TextChannel + await interaction.channel.setParent(defenseCategory.id) await interaction.channel.permissionOverwrites.edit( interaction.guild.id, - { - SendMessages: false, - }, + { SendMessages: false }, ) - await interaction.channel.send( - "Channel unarchived and move backed to defense category", + await interaction.reply( + "**Channel unarchived and move backed to defense category**", ) - robot.logger.info("Channel uarchived and moved.") + robot.logger.info("Channel unarchived and moved.") } } } catch (error) { robot.logger.error(`An error occurred: ${error}`) - await interaction.reply( - `An error occurred: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ) } }) } From 389a9989c0748e441af4ea13b2633627018d45eb Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 3 Jul 2024 11:17:05 +0300 Subject: [PATCH 8/8] Restrict channel categories This resolves the issue with running archiver on specific channel category. Now it can only be run within the channels defined in `defenseArchiveCategoryName` and `defenseCategoryName` --- discord-scripts/channel-management.ts | 53 +++++++++++++++++++++------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/discord-scripts/channel-management.ts b/discord-scripts/channel-management.ts index 9bd21939..12438521 100644 --- a/discord-scripts/channel-management.ts +++ b/discord-scripts/channel-management.ts @@ -9,6 +9,9 @@ import { } from "discord.js" import { writeFile, unlink } from "fs/promises" +const defenseCategoryName = "defense" +const defenseArchiveCategoryName = "Archive: Defense" + // fetch messages in batch of 100 in-order to go past rate limit. async function fetchAllMessages( channel: TextChannel, @@ -79,6 +82,16 @@ export default async function archiveChannel( } try { + const channelCategory = interaction.channel.parent + + if (!channelCategory || channelCategory.name !== defenseCategoryName) { + await interaction.reply({ + content: `**This command can only be run in channels under the ${defenseCategoryName} channel category**`, + ephemeral: true, + }) + return + } + const allMessages = await fetchAllMessages(interaction.channel) robot.logger.info(`Total messages fetched: ${allMessages}`) @@ -99,11 +112,11 @@ export default async function archiveChannel( let archivedCategory = interaction.guild.channels.cache.find( (c) => c.type === ChannelType.GuildCategory && - c.name.toLowerCase() === "archived-channels", + c.name === defenseArchiveCategoryName, ) if (!archivedCategory) { archivedCategory = await interaction.guild.channels.create({ - name: "archived-channels", + name: defenseArchiveCategoryName, type: ChannelType.GuildCategory, }) } @@ -115,9 +128,10 @@ export default async function archiveChannel( interaction.guild.id, { SendMessages: false }, ) - await interaction.reply( - "**Channel archived, locked and moved to #archived-channel category**", - ) + await interaction.reply({ + content: `**Channel archived, locked and moved to ${defenseArchiveCategoryName} channel category**`, + ephemeral: true, + }) // upload chat transcript to channel and then delete it const fileAttachment = new AttachmentBuilder(filePath) @@ -153,6 +167,19 @@ export default async function archiveChannel( } try { if (interaction.channel instanceof TextChannel) { + const channelCategory = interaction.channel.parent + + if ( + !channelCategory || + channelCategory.name !== defenseArchiveCategoryName + ) { + await interaction.reply({ + content: `**This command can only be run in channels under the ${defenseArchiveCategoryName} channel category.**`, + ephemeral: true, + }) + return + } + const defenseCategory = interaction.guild.channels.cache.find( (c) => c.type === ChannelType.GuildCategory && @@ -160,9 +187,10 @@ export default async function archiveChannel( ) if (!defenseCategory) { - await interaction.reply( - "No defense category found to move channel to", - ) + await interaction.reply({ + content: "No defense category found to move channel to", + ephemeral: true, + }) } if (defenseCategory) { @@ -171,10 +199,11 @@ export default async function archiveChannel( interaction.guild.id, { SendMessages: false }, ) - await interaction.reply( - "**Channel unarchived and move backed to defense category**", - ) - robot.logger.info("Channel unarchived and moved.") + await interaction.reply({ + content: + "**Channel unarchived and move backed to defense category**", + ephemeral: true, + }) } } } catch (error) {