From 90ee4ad9095e87a0cea3b93e5d2ffccc1570c826 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Wed, 22 Jul 2020 22:33:10 +0200 Subject: [PATCH] Migrate ChannelArchiver to new Plugin structure, isOwner -> pluginUtils --- backend/src/pluginUtils.ts | 10 ++ .../ChannelArchiver/ChannelArchiverPlugin.ts | 16 +++ .../commands/ArchiveChannelCmd.ts | 110 ++++++++++++++++++ .../ChannelArchiver/rehostAttachment.ts | 29 +++++ backend/src/plugins/ChannelArchiver/types.ts | 7 ++ backend/src/plugins/availablePlugins.ts | 2 + 6 files changed, 174 insertions(+) create mode 100644 backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts create mode 100644 backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts create mode 100644 backend/src/plugins/ChannelArchiver/rehostAttachment.ts create mode 100644 backend/src/plugins/ChannelArchiver/types.ts diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 89184e7e..d23f3d6d 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -66,3 +66,13 @@ export function getBaseUrl(pluginData: PluginData) { const knub = pluginData.getKnubInstance() as TZeppelinKnub; return knub.getGlobalConfig().url; } + +export function isOwner(pluginData: PluginData, userId: string) { + const knub = pluginData.getKnubInstance() as TZeppelinKnub; + const owners = knub.getGlobalConfig().owners; + if (!owners) { + return false; + } + + return owners.includes(userId); +} diff --git a/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts new file mode 100644 index 00000000..599977db --- /dev/null +++ b/backend/src/plugins/ChannelArchiver/ChannelArchiverPlugin.ts @@ -0,0 +1,16 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { ChannelArchiverPluginType } from "./types"; +import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd"; + +export const ChannelArchiverPlugin = zeppelinPlugin()("channel_archiver", { + showInDocs: false, + + // prettier-ignore + commands: [ + ArchiveChannelCmd, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + }, +}); diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts new file mode 100644 index 00000000..61eff881 --- /dev/null +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -0,0 +1,110 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { channelArchiverCmd } from "../types"; +import { isOwner, sendErrorMessage } from "src/pluginUtils"; +import { confirm, SECONDS, noop } from "src/utils"; +import moment from "moment-timezone"; +import { rehostAttachment } from "../rehostAttachment"; + +const MAX_ARCHIVED_MESSAGES = 5000; +const MAX_MESSAGES_PER_FETCH = 100; +const PROGRESS_UPDATE_INTERVAL = 5 * SECONDS; + +export const ArchiveChannelCmd = channelArchiverCmd({ + trigger: "archive_channel", + permission: null, + + config: { + preFilters: [ + (command, context) => { + return isOwner(context.pluginData, context.message.author.id); + }, + ], + }, + + signature: { + channel: ct.textChannel(), + + "attachment-channel": ct.textChannel({ option: true }), + messages: ct.number({ option: true }), + }, + + async run({ message: msg, args, pluginData }) { + if (!args["attachment-channel"]) { + const confirmed = await confirm( + pluginData.client, + msg.channel, + msg.author.id, + "No `-attachment-channel` specified. Continue? Attachments will not be available in the log if their message is deleted.", + ); + if (!confirmed) { + sendErrorMessage(pluginData, msg.channel, "Canceled"); + return; + } + } + + const maxMessagesToArchive = args.messages ? Math.min(args.messages, MAX_ARCHIVED_MESSAGES) : MAX_ARCHIVED_MESSAGES; + if (maxMessagesToArchive <= 0) return; + + const archiveLines = []; + let archivedMessages = 0; + let previousId; + + const startTime = Date.now(); + const progressMsg = await msg.channel.createMessage("Creating archive..."); + const progressUpdateInterval = setInterval(() => { + const secondsSinceStart = Math.round((Date.now() - startTime) / 1000); + progressMsg + .edit(`Creating archive...\n**Status:** ${archivedMessages} messages archived in ${secondsSinceStart} seconds`) + .catch(() => clearInterval(progressUpdateInterval)); + }, PROGRESS_UPDATE_INTERVAL); + + while (archivedMessages < maxMessagesToArchive) { + const messagesToFetch = Math.min(MAX_MESSAGES_PER_FETCH, maxMessagesToArchive - archivedMessages); + const messages = await args.channel.getMessages(messagesToFetch, previousId); + if (messages.length === 0) break; + + for (const message of messages) { + const ts = moment.utc(message.timestamp).format("YYYY-MM-DD HH:mm:ss"); + let content = `[${ts}] [${message.author.id}] [${message.author.username}#${ + message.author.discriminator + }]: ${message.content || ""}`; + + if (message.attachments.length) { + if (args["attachment-channel"]) { + const rehostedAttachmentUrl = await rehostAttachment(message.attachments[0], args["attachment-channel"]); + content += `\n-- Attachment: ${rehostedAttachmentUrl}`; + } else { + content += `\n-- Attachment: ${message.attachments[0].url}`; + } + } + + if (message.reactions && Object.keys(message.reactions).length > 0) { + const reactionCounts = []; + for (const [emoji, info] of Object.entries(message.reactions)) { + reactionCounts.push(`${info.count}x ${emoji}`); + } + content += `\n-- Reactions: ${reactionCounts.join(", ")}`; + } + + archiveLines.push(content); + previousId = message.id; + archivedMessages++; + } + } + + clearInterval(progressUpdateInterval); + + archiveLines.reverse(); + + const nowTs = moment().format("YYYY-MM-DD HH:mm:ss"); + + let result = `Archived ${archiveLines.length} messages from #${args.channel.name} at ${nowTs}`; + result += `\n\n${archiveLines.join("\n")}\n`; + + progressMsg.delete().catch(noop); + msg.channel.createMessage("Archive created!", { + file: Buffer.from(result), + name: `archive-${args.channel.name}-${moment().format("YYYY-MM-DD-HH-mm-ss")}.txt`, + }); + }, +}); diff --git a/backend/src/plugins/ChannelArchiver/rehostAttachment.ts b/backend/src/plugins/ChannelArchiver/rehostAttachment.ts new file mode 100644 index 00000000..0b12e360 --- /dev/null +++ b/backend/src/plugins/ChannelArchiver/rehostAttachment.ts @@ -0,0 +1,29 @@ +import { Attachment, TextChannel } from "eris"; +import { downloadFile } from "src/utils"; +import fs from "fs"; +const fsp = fs.promises; + +const MAX_ATTACHMENT_REHOST_SIZE = 1024 * 1024 * 8; + +export async function rehostAttachment(attachment: Attachment, targetChannel: TextChannel): Promise { + if (attachment.size > MAX_ATTACHMENT_REHOST_SIZE) { + return "Attachment too big to rehost"; + } + + let downloaded; + try { + downloaded = await downloadFile(attachment.url, 3); + } catch (e) { + return "Failed to download attachment after 3 tries"; + } + + try { + const rehostMessage = await targetChannel.createMessage(`Rehost of attachment ${attachment.id}`, { + name: attachment.filename, + file: await fsp.readFile(downloaded.path), + }); + return rehostMessage.attachments[0].url; + } catch (e) { + return "Failed to rehost attachment"; + } +} diff --git a/backend/src/plugins/ChannelArchiver/types.ts b/backend/src/plugins/ChannelArchiver/types.ts new file mode 100644 index 00000000..cbed3e52 --- /dev/null +++ b/backend/src/plugins/ChannelArchiver/types.ts @@ -0,0 +1,7 @@ +import { BasePluginType, command } from "knub"; + +export interface ChannelArchiverPluginType extends BasePluginType { + state: {}; +} + +export const channelArchiverCmd = command(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 32825725..e09b2b32 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -13,10 +13,12 @@ import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigRelo import { CasesPlugin } from "./Cases/CasesPlugin"; import { MutesPlugin } from "./Mutes/MutesPlugin"; import { TagsPlugin } from "./Tags/TagsPlugin"; +import { ChannelArchiverPlugin } from "./ChannelArchiver/ChannelArchiverPlugin"; // prettier-ignore export const guildPlugins: Array> = [ AutoReactionsPlugin, + ChannelArchiverPlugin, LocateUserPlugin, PersistPlugin, PingableRolesPlugin,