feat: add new ExportMessages plugin (#300)

* feat: add new ExportMessages plugin

* fix: update filename format in exportMessage function to use message ID cos it makes more sense

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
VeygaX 2025-07-07 12:25:58 +01:00 • committed by GitHub
parent 73a274db0d
commit 830b5852f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 119 additions and 0 deletions

View file

@ -0,0 +1,115 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { showItemInFolder } from "@utils/native";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Menu } from "@webpack/common";
import { Message } from "discord-types/general";
const { saveFile } = findByPropsLazy("saveFile", "openFile");
const settings = definePluginSettings({
openFileAfterExport: {
type: OptionType.BOOLEAN,
description: "Open the exported file in the default file handler after export",
default: true
}
});
function formatMessage(message: Message) {
const { author } = message;
const timestamp = new Date(message.timestamp.toString()).toLocaleString();
let content = `[${timestamp}] ${author.username}`;
if (author.discriminator !== "0") {
content += `#${author.discriminator}`;
}
content += `: ${message.content}`;
if (message.attachments?.length > 0) {
content += "\n Attachments:";
message.attachments.forEach(attachment => {
content += `\n - ${attachment.filename} (${attachment.url})`;
});
}
if (message.embeds?.length > 0) {
content += "\n Embeds:";
message.embeds.forEach(embed => {
if (embed.rawTitle) content += `\n Title: ${embed.rawTitle}`;
if (embed.rawDescription) content += `\n Description: ${embed.rawDescription}`;
if (embed.url) content += `\n URL: ${embed.url}`;
});
}
return content;
}
async function exportMessage(message: Message) {
const timestamp = new Date(message.timestamp.toString()).toISOString().split("T")[0];
const filename = `message-${message.id}-${timestamp}.txt`;
const content = formatMessage(message);
try {
if (IS_DISCORD_DESKTOP) {
const data = new TextEncoder().encode(content);
const result = await DiscordNative.fileManager.saveWithDialog(data, filename);
if (result && settings.store.openFileAfterExport) {
showItemInFolder(result);
}
} else { // for web
const file = new File([content], filename, { type: "text/plain" });
saveFile(file);
}
showNotification({
title: "Export Messages",
body: `Message exported successfully as ${filename}`,
icon: "📄"
});
} catch (error) {
showNotification({
title: "Export Messages",
body: "Failed to export message",
icon: "❌"
});
}
}
const messageContextMenuPatch = (children: Array<React.ReactElement<any> | null>, props: { message: Message; }) => {
const { message } = props;
if (!message) return;
children.push(
<Menu.MenuItem
id="export-message"
label="Export Message"
icon={() => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
</svg>
)}
action={() => exportMessage(message)}
/>
);
};
export default definePlugin({
name: "ExportMessages",
description: "Allows you to export any message to a file",
authors: [EquicordDevs.veygax],
settings,
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -1096,6 +1096,10 @@ export const EquicordDevs = Object.freeze({
name: "Suffocate",
id: 772601756776923187n
},
veygax: {
name: "veygax",
id: 1119938236245094521n
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly