diff --git a/README.md b/README.md index a2a0b4a..633940f 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,27 @@ -# Replugged plugin template +# Invisible Chat -[Use this template](https://github.com/replugged-org/plugin-template/generate) - -## Prerequisites - -- NodeJS -- pnpm: `npm i -g pnpm` -- [Replugged](https://github.com/replugged-org/replugged#installation) +Encrypt your Messages with the power of Stegcloak ## Install -1. [Create a copy of this template](https://github.com/replugged-org/plugin-template/generate) -2. Clone your new repository and cd into it -3. Install dependencies: `pnpm i` -4. Build the plugin: `pnpm run build` -5. Reload Discord to load the plugin - -The unmodified plugin will log "Typing prevented" in the console when you start typing in any -channel. - -## Development - -The code must be rebuilt after every change. You can use `pnpm run watch` to automatically rebuild -the plugin when you save a file. - -Building using the script above will automatically install the updated version of the plugin in -Replugged. You can find the plugin folder directories for your OS -[here](https://github.com/replugged-org/replugged#installing-plugins-and-themes). -If you don't want to install the updated version, set the `NO_INSTALL` environment variable with any -value: `NO_INSTALL=true pnpm run build`. - -You can format the code by running `pnpm run lint:fix`. The repository includes VSCode settings to -automatically format on save. +1. Go to "Releases" +2. Download the file "dev.sammcheese.InvisibleChat.asar" +3. Move the file to your plugin folder -API docs coming soon(tm) +## Usage -## Distribution +Right now, you have to use the shortcut (CTRL + J) to toggle Encryption. -For plugin distribution, Replugged uses bundled `.asar` files. Bundled plugins can be installed to -the same plugin folder as listed above. +Once toggled, you need to send a message in this format: -This repository includes a GitHub workflow to compile and publish a release with the asar file. To -trigger it, create a tag with the version number preceded by a `v` (e.g. `v1.0.0`) and push it to -GitHub: - -```sh -git tag v1.0.0 -git push --tags +``` +this is the cover message *this will be hidden* ``` -The Replugged updater (coming soonβ„’) will automatically check for updates on the repository -specified in the manifest. Make sure to update it to point to the correct repository! - -You can manually compile the asar file with `pnpm run build-and-bundle`. - -## Troubleshooting - -### Make sure Replugged is installed and running. - -Open Discord settings and make sure the Replugged tab is there. If not, -[follow these instructions](https://github.com/replugged-org/replugged#installation) to install -Replugged. - -### Make sure the plugin is installed. +Decryption works by pressing the (currently) Transparent button in the Minipopover -Check the [plugin folder](https://github.com/replugged-org/replugged#installing-plugins-and-themes) -for your OS and make sure the plugin is there. If not, make sure you have built the plugin and that -the `NO_INSTALL` environment variable is not set. -You can run `replugged.plugins.list().then(console.log)` in the console to see a list of plugins in -the plugin folder. +Encrypted messages look like πŸ”’thisπŸ”’ -### Make sure the plugin is running. +## Credits -Check the console for a message saying `[Replugged:Plugin:Plugin Template] Plugin started`. If you -don't see it, try reloading Discord. If that doesn't work, check for any errors in the console. +None of this would have been possible without [0x41c](https://github.com/0x41c) diff --git a/manifest.json b/manifest.json index 39dcd1e..b32cb10 100644 --- a/manifest.json +++ b/manifest.json @@ -7,7 +7,7 @@ "discordID": "372148345894076416", "github": "SammCheese" }, - "version": "0.1.0", + "version": "1.0.0", "updater": { "type": "github", "id": "SammCheese/invisible-chat" diff --git a/package.json b/package.json index 0fbe00c..df9033d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "plugin-template", + "name": "Invisible Chat", "version": "1.0.0", - "description": "A plugin template", + "description": "Encrypt your Discord Messages", "engines": { "node": ">=14.0.0" }, @@ -34,7 +34,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-react": "^7.31.10", "prettier": "^2.8.1", - "replugged": "4.0.0-beta0.15", + "replugged": "4.0.0-beta0.17", "tsx": "^3.10.3", "typescript": "^4.8.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97b834f..182249e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ specifiers: eslint-plugin-react: ^7.31.10 prettier: ^2.8.1 react: ^18.2.0 - replugged: 4.0.0-beta0.15 + replugged: 4.0.0-beta0.17 tsx: ^3.10.3 typescript: ^4.8.4 @@ -34,7 +34,7 @@ devDependencies: eslint-plugin-node: 11.1.0_eslint@8.25.0 eslint-plugin-react: 7.31.10_eslint@8.25.0 prettier: 2.8.1 - replugged: 4.0.0-beta0.15 + replugged: 4.0.0-beta0.17 tsx: 3.10.4 typescript: 4.8.4 @@ -1554,8 +1554,8 @@ packages: engines: {node: '>=8'} dev: true - /replugged/4.0.0-beta0.15: - resolution: {integrity: sha512-PaEBZ4BxuE0+wEDXDiNokhLiV7gLdhy83t+VkKTUzILDyBCAKSdTqIWCPYh5uFzXkIwqzr3zsEI1G1yOWdv0cQ==} + /replugged/4.0.0-beta0.17: + resolution: {integrity: sha512-x/9uuEOP6swObrTzLp74NquJnHn99NRInTmL/JG3+itSiwDFVmVObZGcXCq5WXo3Uu/G/1sqoMC+OhrN27+fkQ==} engines: {node: '>=14.0.0'} dependencies: react: 18.2.0 diff --git a/src/assets/chatbarLock.tsx b/src/assets/chatbarLock.tsx new file mode 100644 index 0000000..cac7afb --- /dev/null +++ b/src/assets/chatbarLock.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { buildEncModal } from "../components/EncryptionModal"; + +export const chatbarLock = React.createElement( + "svg", + { + key: "Encrypt Message", + fill: "#EBEBEB", + width: "30", + height: "30", + viewBox: "0 0 64 64", + style: { marginTop: 7 }, + onClick: () => buildEncModal(), + }, + React.createElement("path", { + d: "M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z", + }), +); diff --git a/src/assets/popoverIcon.tsx b/src/assets/popoverIcon.tsx index b2a57ca..2e56c7e 100644 --- a/src/assets/popoverIcon.tsx +++ b/src/assets/popoverIcon.tsx @@ -1,17 +1,21 @@ import React from "react"; -export const popoverIcon = () => { - - +/*export const popoverIcon = () => { + + ; -}; +};*/ + +export const popoverIcon = () => + React.createElement( + "svg", + { + fill: "#EBEBEB", + width: "24", + height: "24", + viewBox: "0 0 64 64", + }, + React.createElement("path", { + d: "M 32 9 C 24.832 9 19 14.832 19 22 L 19 27.347656 C 16.670659 28.171862 15 30.388126 15 33 L 15 49 C 15 52.314 17.686 55 21 55 L 43 55 C 46.314 55 49 52.314 49 49 L 49 33 C 49 30.388126 47.329341 28.171862 45 27.347656 L 45 22 C 45 14.832 39.168 9 32 9 z M 32 13 C 36.963 13 41 17.038 41 22 L 41 27 L 23 27 L 23 22 C 23 17.038 27.037 13 32 13 z", + }), + ); diff --git a/src/components/DecryptionModal.tsx b/src/components/DecryptionModal.tsx new file mode 100644 index 0000000..766686c --- /dev/null +++ b/src/components/DecryptionModal.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { webpack } from "replugged"; +import { + ModalContent, + ModalFooter, + ModalHeader, + ModalRoot, + ModalSize, + closeModal, + openModal, +} from "./Modals"; + +import { buildEmbed, decrypt } from "../index"; + +let TextInput: any; +let Button: any; +setTimeout(() => { + TextInput = webpack.getByProps(["defaultProps", "Sizes", "contextType"]); + Button = webpack.getByProps(["Hovers", "Looks", "Sizes"]); +}, 1500); + +export function buildDecModal(msg: any) { + let secret: string = msg?.content; + let password: string; + const s = openModal!((props = msg) => ( + + +
Decrypt Message
+ + +
Secret
+ +
Password
+ { + password = e; + }}> +
+ + + + + + )); +} diff --git a/src/components/EncryptionModal.tsx b/src/components/EncryptionModal.tsx new file mode 100644 index 0000000..66929fb --- /dev/null +++ b/src/components/EncryptionModal.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { webpack } from "replugged"; +import { + ModalContent, + ModalFooter, + ModalHeader, + ModalRoot, + ModalSize, + closeModal, + openModal, +} from "./Modals"; + +import { encrypt } from "../index"; + +let TextInput: any; +let Button: any; +setTimeout(() => { + TextInput = webpack.getByProps(["defaultProps", "Sizes", "contextType"]); + Button = webpack.getByProps(["Hovers", "Looks", "Sizes"]); +}, 1500); + +export function buildEncModal() { + let secret: string; + let cover: string; + let password: string; + const s = openModal!((props) => ( + + +
Encrypt Message
+
+ +
Secret
+ { + secret = e; + }}> +
Cover
+ { + cover = e; + }}> +
Password
+ { + password = e; + }}> +
+ + + + +
+ )); +} diff --git a/src/components/Modals.tsx b/src/components/Modals.tsx new file mode 100644 index 0000000..b334ee5 --- /dev/null +++ b/src/components/Modals.tsx @@ -0,0 +1,123 @@ +/* eslint-disable no-unused-vars */ +// Lots of shit is from VENCORD +// https://github.com/Vendicated/Vencord +// https://github.com/Vendicated/Vencord/blob/main/src/utils/modal.tsx + +import { webpack } from "replugged"; +import React from "react"; + +enum ModalTransitionState { + ENTERING, + ENTERED, + EXITING, + EXITED, + HIDDEN, +} + +export enum ModalSize { + SMALL = "small", + MEDIUM = "medium", + LARGE = "large", + DYNAMIC = "dynamic", +} + +export interface ModalProps { + transitionState: ModalTransitionState; + onClose(): Promise; +} + +export interface ModalOptions { + modalKey?: string; + onCloseRequest?: () => void; + onCloseCallback?: () => void; +} + +/*interface ModalAPI { + openModal: undefined | ((modal: any) => void); + closeModal: undefined | ((modal: any) => void); +}*/ + +interface Modals { + ModalRoot: any; + ModalHeader: any; + ModalContent: any; + ModalFooter: any; + ModalCloseButton: any; +} + +interface ModalRootProps { + transitionState?: ModalTransitionState; + children: React.ReactNode; + size?: ModalSize; + role?: "alertdialog" | "dialog"; + className?: string; + onAnimationEnd?(): string; +} + +type RenderFunction = (props: ModalProps) => React.ReactNode; + +export let ModalAPI: any; +export let Modals: Modals; + +try { + setTimeout(() => { + // Populate ModalAPI + ModalAPI = { + openModal: webpack.getFunctionBySource( + "onCloseRequest:null!=", + webpack.getBySource("onCloseRequest:null!=")!, + ), + closeModal: webpack.getFunctionBySource( + "onCloseCallback&&", + webpack.getBySource("onCloseRequest:null!=")!, + ), + }; + // Populate Modal Types + + Modals = { + ModalRoot: webpack.getFunctionBySource( + "().root", + webpack.getBySource("().closeWithCircleBackground")!, + ), + ModalHeader: webpack.getFunctionBySource( + "().header", + webpack.getBySource("().closeWithCircleBackground")!, + ), + ModalContent: webpack.getFunctionBySource( + "().content", + webpack.getBySource("().closeWithCircleBackground")!, + ), + ModalFooter: webpack.getFunctionBySource( + "().footerSeparator", + webpack.getBySource("().closeWithCircleBackground")!, + ), + ModalCloseButton: webpack.getFunctionBySource( + "().closeWithCircleBackground", + webpack.getBySource("().closeWithCircleBackground")!, + ), + }; + }, 1500); +} catch (e) { + console.log(e); +} + +export const ModalRoot = (props: ModalRootProps) => ; +export const ModalHeader = (props: any) => ; +export const ModalContent = (props: any) => ; +export const ModalFooter = (props: any) => ; +export const ModalCloseButton = (props: any) => ; + +export function openModal( + render: RenderFunction, + options?: ModalOptions, + contextKey?: string, +): void { + return ModalAPI.openModal(render, options, contextKey); +} + +/** + * Close a modal by its key + */ +export function closeModal(modalKey: string, contextKey?: string): void { + return ModalAPI.closeModal(modalKey, contextKey); +} diff --git a/src/index.ts b/src/index.ts index d6d0a59..c75c816 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,32 @@ -import { Injector, OutgoingMessage, settings, webpack } from "replugged"; +import { Injector, OutgoingMessage, webpack } from "replugged"; import StegCloak from "./lib/stegcloak.js"; import { popoverIcon } from "./assets/popoverIcon"; +import { chatbarLock } from "./assets/chatbarLock"; + +import { buildDecModal } from "./components/DecryptionModal"; const inject = new Injector(); +const steggo: StegCloak = new StegCloak(true, false); -interface IncomingMessage extends OutgoingMessage { - embeds: unknown[]; - canPin: undefined | boolean; -} interface StegCloak { hide: (secret: string, password: unknown, cover: string) => string; reveal: (secret: string, password: unknown) => string; } +interface IncomingMessage extends OutgoingMessage { + embeds: unknown[]; + canPin: undefined | boolean; +} -let activeHotkey = false; -const steggo: StegCloak = new StegCloak(true, false); +const EMBED_URL = "https://embed.sammcheese.net"; const INV_DETECTION = new RegExp(/( \u200c|\u200d |[\u2060-\u2064])[^\u200b]/); +const URL_DETECTION = new RegExp( + /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, +); +// eslint-disable-next-line @typescript-eslint/require-await export async function start(): Promise { console.log("%c [Invisible Chat] Started!", "color: aquamarine"); - document.addEventListener("keypress", keypress); - - await injectSendMessages(); // Register the Message Receiver // @ts-expect-error We are adding to Window @@ -30,40 +34,30 @@ export async function start(): Promise { popoverIcon, INV_DETECTION, receiver, + chatbarLock, }; } -function keypress(e: KeyboardEvent): void { - if (e.ctrlKey && e.key === "j") { - activeHotkey = !activeHotkey; - const bar: HTMLElement | null = document.querySelector( - ".scrollableContainer-15eg7h.webkit-QgSAqd", - ); - - if (!bar) return; - - if (activeHotkey) { - bar.style.border = "1px solid"; - bar.style.borderColor = "#09FFFF"; - } else { - bar.style.border = ""; - bar.style.borderColor = ""; - } - } -} - export function runPlaintextPatches(): void { webpack.patchPlaintext([ { replacements: [ { + // Minipopover Lock match: /.\?(..)\(\{key:"reply",label:.{1,40},icon:.{1,40},channel:(.{1,3}),message:(.{1,3}),onClick:.{1,5}\}\):null/gm, replace: `$&,$3.content.match(window.invisiblechat.INV_DETECTION)?$1({key:"decrypt",label:"Decrypt Message",icon:window.invisiblechat.popoverIcon,channel:$2,message:$3,onClick:()=>window.invisiblechat.receiver($3)}):null`, }, { + // Detection Lock + // TODO: Find a better way that doesnt need hovering over the message match: /var .=(.)\.channel,.=.\.message,.=.\.expanded,.=.\.canCopy/gm, - replace: `window.invisiblechat.receiver($1.message);$&`, + replace: `window.invisiblechat.receiver($1.message, $1.canPin);$&`, + }, + { + // Chatbar Lock + match: /.=.\.activeCommand,.=.\.activeCommandOption,(.)=\[\];/, + replace: "$&;$1.push(window.invisiblechat.chatbarLock);", }, ], }, @@ -72,21 +66,40 @@ export function runPlaintextPatches(): void { // Grab the data from the above Plantext Patches function receiver(message: IncomingMessage, canPin: boolean | undefined): void { - if (typeof canPin === "undefined") { + if (typeof canPin !== "undefined") { if (message.content.match(INV_DETECTION) && !message.content.includes("πŸ”’")) { message.content = `πŸ”’${message.content}πŸ”’`; } } else { - void buildEmbed(message); + buildDecModal(message); } } -async function buildEmbed(message: IncomingMessage): Promise { - const password = - (await settings.get("dev.sammcheese.InvisibleChat").get("defaultPassword")) ?? "password"; +// Gets the Embed of a Link +async function getEmbed(url: URL): Promise { + const controller = new AbortController(); + const _timeout = setTimeout(() => controller.abort(), 5000); - // eslint-disable-next-line no-irregular-whitespace - const revealed = steggo.reveal(message.content.replace("​", ""), password); + const options = { + signal: controller.signal, + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url, + }), + }; + + const rawRes = await fetch(EMBED_URL, options); + return await rawRes.json(); +} + +export async function buildEmbed(message: IncomingMessage, revealed: string): Promise { + const urlCheck = revealed.match(URL_DETECTION) || []; + + let attachment; + if (urlCheck[0]) attachment = await getEmbed(new URL(urlCheck[0])); let embed = { type: "rich", @@ -99,6 +112,7 @@ async function buildEmbed(message: IncomingMessage): Promise { }; message.embeds.push(embed); + if (attachment) message.embeds.push(attachment); updateMessage(message); return Promise.resolve(); } @@ -110,30 +124,15 @@ function updateMessage(message: OutgoingMessage): void { }); } -function injectSendMessages(): Promise { - // @ts-expect-error Type Mismatch - inject.before(webpack.common.messages, "sendMessage", async (args: OutgoingMessage[]) => { - if (activeHotkey) { - try { - const { content } = args[1]; - const cover = content.match(/(.{0,2000})\*.{0,2000}\*/)![1]; - const hidden = content.match(/\*.{0,2000}\*/)![0].replaceAll("*", ""); - const pw = - (await settings.get("dev.sammcheese.InvisibleChat").get("defaultPassword")) ?? "password"; - - args[1].content = steggo.hide(hidden, pw, cover); - } catch (e) { - console.log(e); - // DO NOT SEND THE UNENCRYPTED MESSAGE UNDER ANY CIRCUMSTANCE - args[1].content = ""; - } - } - return args; - }); - return Promise.resolve(); -} - export function stop(): void { inject.uninjectAll(); - document.removeEventListener("keypress", keypress); +} + +export function encrypt(secret: string, password: string, cover: string): string { + return steggo.hide(secret, password, cover); +} + +export function decrypt(secret: string, password: string): string { + // eslint-disable-next-line no-irregular-whitespace + return steggo.reveal(secret, password).replace("​", ""); }