/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2023 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; import { buildEncModal } from "./components/EncryptionModal"; let steggo: any; function PopOverIcon() { return ( ); } function Indicator() { return ( {({ onMouseEnter, onMouseLeave }) => ( )} ); } const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { if (!isMainChat) return null; return ( buildEncModal()} buttonProps={{ "aria-haspopup": "dialog", }} > ); }; const settings = definePluginSettings({ savedPasswords: { type: OptionType.STRING, default: "password, Password", description: "Saved Passwords (Seperated with a , )" } }); export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], patches: [ { // Indicator find: ".Messages.MESSAGE_EDITED,", replacement: { match: /let\{className:\i,message:\i[^}]*\}=(\i)/, replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" } }, ], EMBED_API_URL: "https://embed.sammcheese.net", INV_REGEX: new RegExp(/( \u200c|\u200d |[\u2060-\u2064])[^\u200b]/), URL_REGEX: new RegExp( /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, ), settings, async start() { addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) ? { label: "Decrypt Message", icon: this.popOverIcon, message: message, channel: ChannelStore.getChannel(message.channel_id), onClick: async () => { await iteratePasswords(message).then((res: string | false) => { if (res) return void this.buildEmbed(message, res); return void buildDecModal({ message }); }); } } : null; }); addChatBarButton("InvisibleChat", ChatBarIcon); const { default: StegCloak } = await getStegCloak(); steggo = new StegCloak(true, false); }, stop() { removeButton("InvisibleChat"); removeButton("InvisibleChat"); }, // Gets the Embed of a Link async getEmbed(url: URL): Promise { const { body } = await RestAPI.post({ url: Constants.Endpoints.UNFURL_EMBED_URLS, body: { urls: [url] } }); return await body.embeds[0]; }, async buildEmbed(message: any, revealed: string): Promise { const urlCheck = revealed.match(this.URL_REGEX); message.embeds.push({ type: "rich", title: "Decrypted Message", color: "0x45f5f5", description: revealed, footer: { text: "Made with ❤️ by c0dine and Sammy!", }, }); if (urlCheck?.length) { const embed = await this.getEmbed(new URL(urlCheck[0])); if (embed) message.embeds.push(embed); } this.updateMessage(message); }, updateMessage: (message: any) => { FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message, }); }, popOverIcon: () => , indicator: ErrorBoundary.wrap(Indicator, { noop: true }) }); export function encrypt(secret: string, password: string, cover: string): string { return steggo.hide(secret + "\u200b", password, cover); } export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string { const decrypted = steggo.reveal(encrypted, password); return removeIndicator ? decrypted.replace("\u200b", "") : decrypted; } export function isCorrectPassword(result: string): boolean { return result.endsWith("\u200b"); } export async function iteratePasswords(message: Message): Promise { const passwords = settings.store.savedPasswords.split(",").map(s => s.trim()); if (!message?.content || !passwords?.length) return false; let { content } = message; // we use an extra variable so we dont have to edit the message content directly if (/^\W/.test(message.content)) content = `d ${message.content}d`; for (let i = 0; i < passwords.length; i++) { const result = decrypt(content, passwords[i], false); if (isCorrectPassword(result)) { return result; } } return false; }