From 79b20307bc3a0696b77e6bdd307bcb7c41a21b00 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 28 Feb 2024 22:10:53 -0800 Subject: [PATCH 01/58] Added encryptcord --- package.json | 3 + pnpm-lock.yaml | 98 +++++- src/plugins/Encryptcord/index.tsx | 516 ++++++++++++++++++++++++++++++ 3 files changed, 606 insertions(+), 11 deletions(-) create mode 100644 src/plugins/Encryptcord/index.tsx diff --git a/package.json b/package.json index dde55d3114..13017b7925 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "@sapphi-red/web-noise-suppressor": "0.3.3", "@vap/core": "0.0.12", "@vap/shiki": "0.10.5", + "axios": "^1.6.7", "eslint-plugin-simple-header": "^1.0.2", "fflate": "^0.7.4", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "monaco-editor": "^0.43.0", "nanoid": "^4.0.2", + "node-forge": "^1.3.1", "virtual-merge": "^1.0.1" }, "devDependencies": { @@ -46,6 +48,7 @@ "@types/diff": "^5.0.3", "@types/lodash": "^4.14.194", "@types/node": "^18.16.3", + "@types/node-forge": "^1.3.11", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.1", "@types/yazl": "^2.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43866f50ba..a0cc197b92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + patchedDependencies: eslint-plugin-path-alias@1.0.0: hash: m6sma4g6bh67km3q6igf6uxaja @@ -18,6 +22,9 @@ dependencies: '@vap/shiki': specifier: 0.10.5 version: 0.10.5 + axios: + specifier: ^1.6.7 + version: 1.6.7 eslint-plugin-simple-header: specifier: ^1.0.2 version: 1.0.2 @@ -33,6 +40,9 @@ dependencies: nanoid: specifier: ^4.0.2 version: 4.0.2 + node-forge: + specifier: ^1.3.1 + version: 1.3.1 virtual-merge: specifier: ^1.0.1 version: 1.0.1 @@ -50,6 +60,9 @@ devDependencies: '@types/node': specifier: ^18.16.3 version: 18.16.3 + '@types/node-forge': + specifier: ^1.3.11 + version: 1.3.11 '@types/react': specifier: ^18.2.0 version: 18.2.0 @@ -572,6 +585,12 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true + /@types/node-forge@1.3.11: + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + dependencies: + '@types/node': 18.16.3 + dev: true + /@types/node@18.16.3: resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} dev: true @@ -613,8 +632,8 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true - /@types/yauzl@2.10.0: - resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: '@types/node': 18.16.3 @@ -883,12 +902,26 @@ packages: resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} hasBin: true dev: true + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1067,6 +1100,13 @@ packages: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} dev: true + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /component-emitter@1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true @@ -1197,6 +1237,11 @@ packages: isobject: 3.0.1 dev: true + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /devtools-protocol@0.0.1107588: resolution: {integrity: sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==} dev: true @@ -1699,7 +1744,7 @@ packages: get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: - '@types/yauzl': 2.10.0 + '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color dev: true @@ -1790,11 +1835,30 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} dev: true + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /fragment-cache@0.2.1: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} @@ -1810,8 +1874,8 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -2364,6 +2428,18 @@ packages: picomatch: 2.3.1 dev: true + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2467,6 +2543,11 @@ packages: whatwg-url: 5.0.0 dev: true + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: false + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -2678,7 +2759,6 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: true /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -3227,7 +3307,7 @@ packages: '@esbuild-kit/core-utils': 3.1.0 '@esbuild-kit/esm-loader': 2.5.5 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /type-check@0.4.0: @@ -3464,7 +3544,3 @@ packages: name: gifenc version: 1.0.3 dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/src/plugins/Encryptcord/index.tsx b/src/plugins/Encryptcord/index.tsx new file mode 100644 index 0000000000..0ec99cc852 --- /dev/null +++ b/src/plugins/Encryptcord/index.tsx @@ -0,0 +1,516 @@ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; +import { removeButton } from "@api/MessagePopover"; +import definePlugin, { StartAt } from "@utils/types"; +import * as DataStore from "@api/DataStore"; +import { sleep } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; +import { useEffect, useState } from "@webpack/common"; +import { + RestAPI, + SnowflakeUtils, + UserUtils, + UserStore, + MessageActions, +} from "@webpack/common"; +import { + ApplicationCommandInputType, + sendBotMessage, + ApplicationCommandOptionType, + findOption, +} from "@api/Commands"; +import { Message } from "discord-types/general"; +const MessageCreator = findByPropsLazy("createBotMessage"); +const CloudUtils = findByPropsLazy("CloudUpload"); +import axios from 'axios'; +import { getCurrentChannel } from "@utils/discord"; +import forge from 'node-forge'; + +let enabled; +let setEnabled; + +// Interface for Message Create +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + message: Message; +} + +// Generate RSA key pair +function generateKeyPair(): { privateKey: string; publicKey: string; } { + const keys = forge.pki.rsa.generateKeyPair({ bits: 1024 }); + const privateKey = forge.pki.privateKeyToPem(keys.privateKey); + const publicKey = forge.pki.publicKeyToPem(keys.publicKey); + + return { privateKey, publicKey }; +} + +// Encrypt message with public key +function encrypt(message: string, publicKey): string[] { + try { + const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); + const chunkSize = 62; + + const encryptedChunks: string[] = []; + + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.substring(i, i + chunkSize); + const encryptedChunk = publicKeyObj.encrypt(chunk, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + encryptedChunks.push(forge.util.encode64(encryptedChunk)); + } + + return encryptedChunks; + } catch (error) { + return []; + } +} + +// Decrypt message with private key +function decrypt(encryptedMessages: string[], privateKey): string { + const privateKeyObj = forge.pki.privateKeyFromPem(privateKey); + let decryptedMessages: string[] = []; + + encryptedMessages.forEach((encryptedMessage) => { + const encrypted = forge.util.decode64(encryptedMessage); + const decrypted = privateKeyObj.decrypt(encrypted, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + decryptedMessages.push(decrypted); + }); + + return decryptedMessages.join(''); +} + +// Chat Bar Icon Component +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + [enabled, setEnabled] = useState(false); + + useEffect(() => { + const listener: SendListener = async (_, message) => { + if (enabled) { + const groupChannel = await DataStore.get('encryptcordChannelId'); + if (getCurrentChannel().id !== groupChannel) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` }); + message.content = ""; + return; + } + const trimmedMessage = message.content.trim(); + await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0)); + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + const encryptedMessage = encrypt(trimmedMessage, encryptcordGroupMembers[memberId]); + const encryptedMessageString = JSON.stringify(encryptedMessage); + await sendTempMessage(groupMember.id, encryptedMessageString, `message`); + }); + + await Promise.all(dmPromises); + message.content = ""; + } + }; + + addPreSendListener(listener); + return () => void removePreSendListener(listener); + }, [enabled]); + + if (!isMainChat) return null; + + return ( + { + const groupChannel = await DataStore.get('encryptcordChannelId'); + if (await DataStore.get('encryptcordGroup') == false) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in an E2EE group to send an encrypted message!` }); + return; + } + if (getCurrentChannel().id !== groupChannel) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in the E2EE group channel to send an encrypted message!` }); + return; + } + setEnabled(!enabled); + }} + buttonProps={{ + "aria-haspopup": "dialog", + }} + > + + {!enabled && <> + + + + + } + + + + ); +}; + +// Export Plugin +export default definePlugin({ + name: "Encryptcord", + description: "End-to-end encryption in Discord!", + authors: [ + { + id: 761777382041714690n, + name: "Inbestigator", + }, + ], + dependencies: ["CommandsAPI"], + patches: [ + { + find: "executeMessageComponentInteraction:", + replacement: { + match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, + replace: 'await $self.joinGroup(C);$&' + } + } + ], + async joinGroup(interaction) { + const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); + if (!sender || sender.bot == true) return; + if (interaction.data.component_type == 2 && interaction.data.custom_id == "acceptGroup") { + await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); + } + }, + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content) return; + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + if (!encryptcordGroupJoinList.includes(message.author.id)) { + switch (message.content.split("/")[0].toLowerCase()) { + case "e2eeinvite": + const inviteMessage = `I've invited you to an [end-to-end encrypted]() group in <#${message.content.split("/")[1]}>.`; + await MessageActions.receiveMessage(channelId, { + ...await createMessage(inviteMessage, message.author.id, channelId, 0), components: [{ + type: 1, + components: [{ + type: 2, + style: 3, + label: 'Accept!', + custom_id: 'acceptGroup' + }] + }] + }); + break; + case "groupdata": + const groupdata = (await axios.get(message.attachments[0].url)).data; + await handleGroupData(groupdata); + break; + default: + break; + } + return; + }; + if (message.content.toLowerCase() !== "join") return; + const sender = await UserUtils.getUser(message.author.id).catch(() => null); + if (!sender) return; + const userKey = (await axios.get(message.attachments[0].url)).data; + await handleJoin(sender.id, userKey, encryptcordGroupMembers); + return; + } + const dmChannelId = await RestAPI.post({ + url: `/users/@me/channels`, + body: { + recipient_id: message.author.id, + }, + }).then((response) => response.body.id); + if (channelId !== dmChannelId) return; + const sender = await UserUtils.getUser(message.author.id).catch(() => null); + if (!sender) return; + const groupChannel = await DataStore.get('encryptcordChannelId'); + switch (message.content.toLowerCase()) { + case "leaving": + handleLeaving(sender.id, encryptcordGroupMembers, groupChannel); + break; + case "message": + const messagedata = (await axios.get(message.attachments[0].url)).data; + await handleMessage(messagedata, sender.id, groupChannel); + break; + case "groupdata": + const groupdata = (await axios.get(message.attachments[0].url)).data; + await handleGroupData(groupdata); + break; + default: + break; + } + }, + }, + commands: [ + { + name: "encryptcord", + description: "End-to-end encryption in Discord!", + options: [ + { + name: "leave", + description: "Leave current group", + options: [], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + { + name: "start", + description: "Start an E2EE group", + options: [], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + { + name: "invite", + description: "Invite a user to your group", + options: [ + { + name: "user", + description: "Who to invite", + required: true, + type: ApplicationCommandOptionType.USER, + }, + ], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + ], + inputType: ApplicationCommandInputType.BOT, + execute: (opts, ctx) => { + switch (opts[0].name) { + case "start": + startGroup(opts[0].options, ctx); + break; + case "invite": + invite(opts[0].options, ctx); + break; + case "leave": + leave(opts[0].options, ctx); + break; + } + }, + }, + ], + startAt: StartAt.DOMContentLoaded, + async start() { + addChatBarButton("Encryptcord", ChatBarIcon); + const pair = generateKeyPair(); + await DataStore.set('encryptcordPublicKey', pair.publicKey); + await DataStore.set('encryptcordPrivateKey', pair.privateKey); + if (await DataStore.get("encryptcordGroup") == true) { + await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); + } + await DataStore.set('encryptcordGroup', false); + await DataStore.set('encryptcordChannelId', ""); + await DataStore.set('encryptcordGroupMembers', {}); + await DataStore.set('encryptcordGroupJoinList', []); + }, + stop() { + removeButton("Encryptcord"); + }, +}); + +// Send Temporary Message +async function sendTempMessage(recipientId: string, attachment: string, content: string) { + if (recipientId == UserStore.getCurrentUser().id) return; + + const dmChannelId = await RestAPI.post({ + url: `/users/@me/channels`, + body: { + recipient_id: recipientId, + }, + }).then((response) => response.body.id); + + if (attachment && attachment != "") { + const upload = await new CloudUtils.CloudUpload({ + file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }), + isClip: false, + isThumbnail: false, + platform: 1, + }, dmChannelId, false, 0); + upload.on("complete", async () => { + const messageId = await RestAPI.post({ + url: `/channels/${dmChannelId}/messages`, + body: { + content, + attachments: [{ + id: "0", + filename: upload.filename, + uploaded_filename: upload.uploadedFilename, + }], + nonce: SnowflakeUtils.fromTimestamp(Date.now()), + }, + }).then((response) => response.body.id); + + await sleep(500); + RestAPI.delete({ + url: `/channels/${dmChannelId}/messages/${messageId}` + }); + }); + await upload.upload(); + return; + } + + const messageId = await RestAPI.post({ + url: `/channels/${dmChannelId}/messages`, + body: { + content, + nonce: SnowflakeUtils.fromTimestamp(Date.now()), + }, + }).then((response) => response.body.id); + + await sleep(500); + RestAPI.delete({ + url: `/channels/${dmChannelId}/messages/${messageId}` + }); +} + +// Handle leaving group +async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) { + const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => { + if (memberId !== senderId) { + result[memberId] = encryptcordGroupMembers[memberId]; + } + return result; + }, {}); + + await DataStore.set('encryptcordGroupMembers', updatedMembers); + + await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2)); +} + +// Handle receiving message +async function handleMessage(message, senderId: string, groupChannel: string) { + const decryptedMessage = decrypt(message, await DataStore.get("encryptcordPrivateKey")); + await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); +} + +// Handle receiving group data +async function handleGroupData(groupData) { + await DataStore.set('encryptcordChannelId', groupData.channel); + await DataStore.set('encryptcordGroupMembers', groupData.members); + await DataStore.set('encryptcordGroup', true); + await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7)); + setEnabled(true); +} + +// Handle joining group +async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) { + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + const updatedMembers = encryptcordGroupJoinList.filter(memberId => memberId !== senderId); + await DataStore.set('encryptcordGroupJoinList', updatedMembers); + + encryptcordGroupMembers[senderId] = senderKey; + await DataStore.set('encryptcordGroupMembers', encryptcordGroupMembers); + const groupChannel = await DataStore.get('encryptcordChannelId'); + const newMember = await UserUtils.getUser(senderId).catch(() => null); + if (!newMember) return; + + const membersData = {}; + Object.entries(encryptcordGroupMembers) + .forEach(([memberId, value]) => { + membersData[memberId] = value; + }); + + const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel }); + + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + await sendTempMessage(groupMember.id, membersDataString, `groupdata`); + }); + + await Promise.all(dmPromises); + await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 7)); +} + +// Create message for group +async function createMessage(message: string, senderId: string, channelId: string, type: number) { + const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); + const sender = await UserUtils.getUser(senderId).catch(() => null); + if (!sender) return; + return { ...messageStart, content: message, author: sender, type, flags: 0 }; +} + +// Start E2EE Group +async function startGroup(opts, ctx) { + const channelId = ctx.channel.id; + await DataStore.set('encryptcordChannelId', channelId); + await DataStore.set('encryptcordGroupMembers', { + [UserStore.getCurrentUser().id]: await DataStore.get("encryptcordPublicKey") + }); + await DataStore.set('encryptcordGroupJoinList', []); + await DataStore.set('encryptcordGroup', true); + sendBotMessage(channelId, { content: "Group created!" }); + await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); + setEnabled(true); +} + +// Invite User to Group +async function invite(opts, ctx) { + const invitedUser = await UserUtils.getUser(findOption(opts, "user", "")).catch(() => null); + if (!invitedUser) return; + + const channelId = ctx.channel.id; + if (!(await DataStore.get('encryptcordGroup'))) { + sendBotMessage(channelId, { content: `You're not in a group!` }); + return; + } + + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + if (Object.keys(encryptcordGroupMembers).some(key => key == invitedUser.id)) { + sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the group.` }); + return; + } + + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + if (encryptcordGroupJoinList.includes(invitedUser.id)) { + sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the join list.` }); + return; + } + + encryptcordGroupJoinList.push(invitedUser.id); + await DataStore.set('encryptcordGroupJoinList', encryptcordGroupJoinList); + + await sendTempMessage(invitedUser.id, "", `e2eeinvite/${await DataStore.get('encryptcordChannelId')}`); + + sendBotMessage(channelId, { content: `<@${invitedUser.id}> invited successfully.` }); +} + +// Leave the Group +async function leave(opts, ctx) { + const channelId = ctx.channel.id; + if (!(await DataStore.get('encryptcordGroup'))) { + sendBotMessage(channelId, { content: `You're not in a group!` }); + return; + } + const user = UserStore.getCurrentUser(); + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + await sendTempMessage(groupMember.id, "", `leaving`); + }); + + await Promise.all(dmPromises); + await DataStore.set('encryptcordGroup', false); + await DataStore.set('encryptcordChannelId', ""); + await DataStore.set('encryptcordGroupMembers', {}); + await DataStore.set('encryptcordGroupJoinList', []); + await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); + setEnabled(false); +} From 622a13cca38a27b898998640410a9219fbe78e78 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 28 Feb 2024 22:34:36 -0800 Subject: [PATCH 02/58] Added myself to devs --- src/plugins/Encryptcord/index.tsx | 8 ++------ src/utils/constants.ts | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/Encryptcord/index.tsx b/src/plugins/Encryptcord/index.tsx index 0ec99cc852..9a0b8bd402 100644 --- a/src/plugins/Encryptcord/index.tsx +++ b/src/plugins/Encryptcord/index.tsx @@ -6,6 +6,7 @@ import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { useEffect, useState } from "@webpack/common"; +import { Devs } from "@utils/constants"; import { RestAPI, SnowflakeUtils, @@ -167,12 +168,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { export default definePlugin({ name: "Encryptcord", description: "End-to-end encryption in Discord!", - authors: [ - { - id: 761777382041714690n, - name: "Inbestigator", - }, - ], + authors: [Devs.Inbestigator], dependencies: ["CommandsAPI"], patches: [ { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d66bdc8260..8d40ed1d0c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -50,6 +50,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Vendicated", id: 343383572805058560n }, + Inbestigator: { + name: "Inbestigator", + id: 761777382041714690n + }, Arjix: { name: "ArjixWasTaken", id: 674710789138939916n From dbe9dda2adab268b2739f2853cdd86077a1271a1 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 28 Feb 2024 23:19:33 -0800 Subject: [PATCH 03/58] 1 --- src/plugins/Encryptcord/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/Encryptcord/index.tsx b/src/plugins/Encryptcord/index.tsx index 9a0b8bd402..da943a9e28 100644 --- a/src/plugins/Encryptcord/index.tsx +++ b/src/plugins/Encryptcord/index.tsx @@ -54,6 +54,9 @@ function encrypt(message: string, publicKey): string[] { const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); const chunkSize = 62; + const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; + message = message.replace(emojiRegex, ''); + const encryptedChunks: string[] = []; for (let i = 0; i < message.length; i += chunkSize) { @@ -175,16 +178,17 @@ export default definePlugin({ find: "executeMessageComponentInteraction:", replacement: { match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, - replace: 'await $self.joinGroup(C);$&' + replace: 'if(await $self.joinGroup(C))return;$&' } } ], async joinGroup(interaction) { const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); - if (!sender || sender.bot == true) return; + if (!sender || sender.bot == true) return false; if (interaction.data.component_type == 2 && interaction.data.custom_id == "acceptGroup") { await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); } + return true; }, flux: { async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { From 11de96a95e7a9812172830e2059daadc51b741d9 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:47:45 -0800 Subject: [PATCH 04/58] Update index.tsx --- src/plugins/Encryptcord/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/Encryptcord/index.tsx b/src/plugins/Encryptcord/index.tsx index da943a9e28..fdf3652452 100644 --- a/src/plugins/Encryptcord/index.tsx +++ b/src/plugins/Encryptcord/index.tsx @@ -41,7 +41,7 @@ interface IMessageCreate { // Generate RSA key pair function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 1024 }); + const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); const privateKey = forge.pki.privateKeyToPem(keys.privateKey); const publicKey = forge.pki.publicKeyToPem(keys.publicKey); @@ -52,7 +52,7 @@ function generateKeyPair(): { privateKey: string; publicKey: string; } { function encrypt(message: string, publicKey): string[] { try { const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 62; + const chunkSize = 190; const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; message = message.replace(emojiRegex, ''); From fb82a2f55c228f582d3ba1d34eaa2260edc6fdd9 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:50:01 -0800 Subject: [PATCH 05/58] Create index.tsx --- src/plugins/encryptcord/index.tsx | 516 ++++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 src/plugins/encryptcord/index.tsx diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx new file mode 100644 index 0000000000..da943a9e28 --- /dev/null +++ b/src/plugins/encryptcord/index.tsx @@ -0,0 +1,516 @@ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; +import { removeButton } from "@api/MessagePopover"; +import definePlugin, { StartAt } from "@utils/types"; +import * as DataStore from "@api/DataStore"; +import { sleep } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; +import { useEffect, useState } from "@webpack/common"; +import { Devs } from "@utils/constants"; +import { + RestAPI, + SnowflakeUtils, + UserUtils, + UserStore, + MessageActions, +} from "@webpack/common"; +import { + ApplicationCommandInputType, + sendBotMessage, + ApplicationCommandOptionType, + findOption, +} from "@api/Commands"; +import { Message } from "discord-types/general"; +const MessageCreator = findByPropsLazy("createBotMessage"); +const CloudUtils = findByPropsLazy("CloudUpload"); +import axios from 'axios'; +import { getCurrentChannel } from "@utils/discord"; +import forge from 'node-forge'; + +let enabled; +let setEnabled; + +// Interface for Message Create +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + message: Message; +} + +// Generate RSA key pair +function generateKeyPair(): { privateKey: string; publicKey: string; } { + const keys = forge.pki.rsa.generateKeyPair({ bits: 1024 }); + const privateKey = forge.pki.privateKeyToPem(keys.privateKey); + const publicKey = forge.pki.publicKeyToPem(keys.publicKey); + + return { privateKey, publicKey }; +} + +// Encrypt message with public key +function encrypt(message: string, publicKey): string[] { + try { + const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); + const chunkSize = 62; + + const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; + message = message.replace(emojiRegex, ''); + + const encryptedChunks: string[] = []; + + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.substring(i, i + chunkSize); + const encryptedChunk = publicKeyObj.encrypt(chunk, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + encryptedChunks.push(forge.util.encode64(encryptedChunk)); + } + + return encryptedChunks; + } catch (error) { + return []; + } +} + +// Decrypt message with private key +function decrypt(encryptedMessages: string[], privateKey): string { + const privateKeyObj = forge.pki.privateKeyFromPem(privateKey); + let decryptedMessages: string[] = []; + + encryptedMessages.forEach((encryptedMessage) => { + const encrypted = forge.util.decode64(encryptedMessage); + const decrypted = privateKeyObj.decrypt(encrypted, 'RSA-OAEP', { + md: forge.md.sha256.create(), + }); + decryptedMessages.push(decrypted); + }); + + return decryptedMessages.join(''); +} + +// Chat Bar Icon Component +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + [enabled, setEnabled] = useState(false); + + useEffect(() => { + const listener: SendListener = async (_, message) => { + if (enabled) { + const groupChannel = await DataStore.get('encryptcordChannelId'); + if (getCurrentChannel().id !== groupChannel) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` }); + message.content = ""; + return; + } + const trimmedMessage = message.content.trim(); + await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0)); + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + const encryptedMessage = encrypt(trimmedMessage, encryptcordGroupMembers[memberId]); + const encryptedMessageString = JSON.stringify(encryptedMessage); + await sendTempMessage(groupMember.id, encryptedMessageString, `message`); + }); + + await Promise.all(dmPromises); + message.content = ""; + } + }; + + addPreSendListener(listener); + return () => void removePreSendListener(listener); + }, [enabled]); + + if (!isMainChat) return null; + + return ( + { + const groupChannel = await DataStore.get('encryptcordChannelId'); + if (await DataStore.get('encryptcordGroup') == false) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in an E2EE group to send an encrypted message!` }); + return; + } + if (getCurrentChannel().id !== groupChannel) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in the E2EE group channel to send an encrypted message!` }); + return; + } + setEnabled(!enabled); + }} + buttonProps={{ + "aria-haspopup": "dialog", + }} + > + + {!enabled && <> + + + + + } + + + + ); +}; + +// Export Plugin +export default definePlugin({ + name: "Encryptcord", + description: "End-to-end encryption in Discord!", + authors: [Devs.Inbestigator], + dependencies: ["CommandsAPI"], + patches: [ + { + find: "executeMessageComponentInteraction:", + replacement: { + match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, + replace: 'if(await $self.joinGroup(C))return;$&' + } + } + ], + async joinGroup(interaction) { + const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); + if (!sender || sender.bot == true) return false; + if (interaction.data.component_type == 2 && interaction.data.custom_id == "acceptGroup") { + await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); + } + return true; + }, + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content) return; + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + if (!encryptcordGroupJoinList.includes(message.author.id)) { + switch (message.content.split("/")[0].toLowerCase()) { + case "e2eeinvite": + const inviteMessage = `I've invited you to an [end-to-end encrypted]() group in <#${message.content.split("/")[1]}>.`; + await MessageActions.receiveMessage(channelId, { + ...await createMessage(inviteMessage, message.author.id, channelId, 0), components: [{ + type: 1, + components: [{ + type: 2, + style: 3, + label: 'Accept!', + custom_id: 'acceptGroup' + }] + }] + }); + break; + case "groupdata": + const groupdata = (await axios.get(message.attachments[0].url)).data; + await handleGroupData(groupdata); + break; + default: + break; + } + return; + }; + if (message.content.toLowerCase() !== "join") return; + const sender = await UserUtils.getUser(message.author.id).catch(() => null); + if (!sender) return; + const userKey = (await axios.get(message.attachments[0].url)).data; + await handleJoin(sender.id, userKey, encryptcordGroupMembers); + return; + } + const dmChannelId = await RestAPI.post({ + url: `/users/@me/channels`, + body: { + recipient_id: message.author.id, + }, + }).then((response) => response.body.id); + if (channelId !== dmChannelId) return; + const sender = await UserUtils.getUser(message.author.id).catch(() => null); + if (!sender) return; + const groupChannel = await DataStore.get('encryptcordChannelId'); + switch (message.content.toLowerCase()) { + case "leaving": + handleLeaving(sender.id, encryptcordGroupMembers, groupChannel); + break; + case "message": + const messagedata = (await axios.get(message.attachments[0].url)).data; + await handleMessage(messagedata, sender.id, groupChannel); + break; + case "groupdata": + const groupdata = (await axios.get(message.attachments[0].url)).data; + await handleGroupData(groupdata); + break; + default: + break; + } + }, + }, + commands: [ + { + name: "encryptcord", + description: "End-to-end encryption in Discord!", + options: [ + { + name: "leave", + description: "Leave current group", + options: [], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + { + name: "start", + description: "Start an E2EE group", + options: [], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + { + name: "invite", + description: "Invite a user to your group", + options: [ + { + name: "user", + description: "Who to invite", + required: true, + type: ApplicationCommandOptionType.USER, + }, + ], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, + ], + inputType: ApplicationCommandInputType.BOT, + execute: (opts, ctx) => { + switch (opts[0].name) { + case "start": + startGroup(opts[0].options, ctx); + break; + case "invite": + invite(opts[0].options, ctx); + break; + case "leave": + leave(opts[0].options, ctx); + break; + } + }, + }, + ], + startAt: StartAt.DOMContentLoaded, + async start() { + addChatBarButton("Encryptcord", ChatBarIcon); + const pair = generateKeyPair(); + await DataStore.set('encryptcordPublicKey', pair.publicKey); + await DataStore.set('encryptcordPrivateKey', pair.privateKey); + if (await DataStore.get("encryptcordGroup") == true) { + await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); + } + await DataStore.set('encryptcordGroup', false); + await DataStore.set('encryptcordChannelId', ""); + await DataStore.set('encryptcordGroupMembers', {}); + await DataStore.set('encryptcordGroupJoinList', []); + }, + stop() { + removeButton("Encryptcord"); + }, +}); + +// Send Temporary Message +async function sendTempMessage(recipientId: string, attachment: string, content: string) { + if (recipientId == UserStore.getCurrentUser().id) return; + + const dmChannelId = await RestAPI.post({ + url: `/users/@me/channels`, + body: { + recipient_id: recipientId, + }, + }).then((response) => response.body.id); + + if (attachment && attachment != "") { + const upload = await new CloudUtils.CloudUpload({ + file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }), + isClip: false, + isThumbnail: false, + platform: 1, + }, dmChannelId, false, 0); + upload.on("complete", async () => { + const messageId = await RestAPI.post({ + url: `/channels/${dmChannelId}/messages`, + body: { + content, + attachments: [{ + id: "0", + filename: upload.filename, + uploaded_filename: upload.uploadedFilename, + }], + nonce: SnowflakeUtils.fromTimestamp(Date.now()), + }, + }).then((response) => response.body.id); + + await sleep(500); + RestAPI.delete({ + url: `/channels/${dmChannelId}/messages/${messageId}` + }); + }); + await upload.upload(); + return; + } + + const messageId = await RestAPI.post({ + url: `/channels/${dmChannelId}/messages`, + body: { + content, + nonce: SnowflakeUtils.fromTimestamp(Date.now()), + }, + }).then((response) => response.body.id); + + await sleep(500); + RestAPI.delete({ + url: `/channels/${dmChannelId}/messages/${messageId}` + }); +} + +// Handle leaving group +async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) { + const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => { + if (memberId !== senderId) { + result[memberId] = encryptcordGroupMembers[memberId]; + } + return result; + }, {}); + + await DataStore.set('encryptcordGroupMembers', updatedMembers); + + await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2)); +} + +// Handle receiving message +async function handleMessage(message, senderId: string, groupChannel: string) { + const decryptedMessage = decrypt(message, await DataStore.get("encryptcordPrivateKey")); + await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); +} + +// Handle receiving group data +async function handleGroupData(groupData) { + await DataStore.set('encryptcordChannelId', groupData.channel); + await DataStore.set('encryptcordGroupMembers', groupData.members); + await DataStore.set('encryptcordGroup', true); + await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7)); + setEnabled(true); +} + +// Handle joining group +async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) { + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + const updatedMembers = encryptcordGroupJoinList.filter(memberId => memberId !== senderId); + await DataStore.set('encryptcordGroupJoinList', updatedMembers); + + encryptcordGroupMembers[senderId] = senderKey; + await DataStore.set('encryptcordGroupMembers', encryptcordGroupMembers); + const groupChannel = await DataStore.get('encryptcordChannelId'); + const newMember = await UserUtils.getUser(senderId).catch(() => null); + if (!newMember) return; + + const membersData = {}; + Object.entries(encryptcordGroupMembers) + .forEach(([memberId, value]) => { + membersData[memberId] = value; + }); + + const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel }); + + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + await sendTempMessage(groupMember.id, membersDataString, `groupdata`); + }); + + await Promise.all(dmPromises); + await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 7)); +} + +// Create message for group +async function createMessage(message: string, senderId: string, channelId: string, type: number) { + const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); + const sender = await UserUtils.getUser(senderId).catch(() => null); + if (!sender) return; + return { ...messageStart, content: message, author: sender, type, flags: 0 }; +} + +// Start E2EE Group +async function startGroup(opts, ctx) { + const channelId = ctx.channel.id; + await DataStore.set('encryptcordChannelId', channelId); + await DataStore.set('encryptcordGroupMembers', { + [UserStore.getCurrentUser().id]: await DataStore.get("encryptcordPublicKey") + }); + await DataStore.set('encryptcordGroupJoinList', []); + await DataStore.set('encryptcordGroup', true); + sendBotMessage(channelId, { content: "Group created!" }); + await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); + setEnabled(true); +} + +// Invite User to Group +async function invite(opts, ctx) { + const invitedUser = await UserUtils.getUser(findOption(opts, "user", "")).catch(() => null); + if (!invitedUser) return; + + const channelId = ctx.channel.id; + if (!(await DataStore.get('encryptcordGroup'))) { + sendBotMessage(channelId, { content: `You're not in a group!` }); + return; + } + + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + if (Object.keys(encryptcordGroupMembers).some(key => key == invitedUser.id)) { + sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the group.` }); + return; + } + + const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); + if (encryptcordGroupJoinList.includes(invitedUser.id)) { + sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the join list.` }); + return; + } + + encryptcordGroupJoinList.push(invitedUser.id); + await DataStore.set('encryptcordGroupJoinList', encryptcordGroupJoinList); + + await sendTempMessage(invitedUser.id, "", `e2eeinvite/${await DataStore.get('encryptcordChannelId')}`); + + sendBotMessage(channelId, { content: `<@${invitedUser.id}> invited successfully.` }); +} + +// Leave the Group +async function leave(opts, ctx) { + const channelId = ctx.channel.id; + if (!(await DataStore.get('encryptcordGroup'))) { + sendBotMessage(channelId, { content: `You're not in a group!` }); + return; + } + const user = UserStore.getCurrentUser(); + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + + const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { + const groupMember = await UserUtils.getUser(memberId).catch(() => null); + if (!groupMember) return; + await sendTempMessage(groupMember.id, "", `leaving`); + }); + + await Promise.all(dmPromises); + await DataStore.set('encryptcordGroup', false); + await DataStore.set('encryptcordChannelId', ""); + await DataStore.set('encryptcordGroupMembers', {}); + await DataStore.set('encryptcordGroupJoinList', []); + await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); + setEnabled(false); +} From a31e2d4cedeaca576b1ff1ead535ce9aa3850c50 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:50:23 -0800 Subject: [PATCH 06/58] Delete src/plugins/Encryptcord directory --- src/plugins/Encryptcord/index.tsx | 516 ------------------------------ 1 file changed, 516 deletions(-) delete mode 100644 src/plugins/Encryptcord/index.tsx diff --git a/src/plugins/Encryptcord/index.tsx b/src/plugins/Encryptcord/index.tsx deleted file mode 100644 index fdf3652452..0000000000 --- a/src/plugins/Encryptcord/index.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; -import { removeButton } from "@api/MessagePopover"; -import definePlugin, { StartAt } from "@utils/types"; -import * as DataStore from "@api/DataStore"; -import { sleep } from "@utils/misc"; -import { findByPropsLazy } from "@webpack"; -import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; -import { useEffect, useState } from "@webpack/common"; -import { Devs } from "@utils/constants"; -import { - RestAPI, - SnowflakeUtils, - UserUtils, - UserStore, - MessageActions, -} from "@webpack/common"; -import { - ApplicationCommandInputType, - sendBotMessage, - ApplicationCommandOptionType, - findOption, -} from "@api/Commands"; -import { Message } from "discord-types/general"; -const MessageCreator = findByPropsLazy("createBotMessage"); -const CloudUtils = findByPropsLazy("CloudUpload"); -import axios from 'axios'; -import { getCurrentChannel } from "@utils/discord"; -import forge from 'node-forge'; - -let enabled; -let setEnabled; - -// Interface for Message Create -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - message: Message; -} - -// Generate RSA key pair -function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); - const privateKey = forge.pki.privateKeyToPem(keys.privateKey); - const publicKey = forge.pki.publicKeyToPem(keys.publicKey); - - return { privateKey, publicKey }; -} - -// Encrypt message with public key -function encrypt(message: string, publicKey): string[] { - try { - const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 190; - - const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; - message = message.replace(emojiRegex, ''); - - const encryptedChunks: string[] = []; - - for (let i = 0; i < message.length; i += chunkSize) { - const chunk = message.substring(i, i + chunkSize); - const encryptedChunk = publicKeyObj.encrypt(chunk, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - encryptedChunks.push(forge.util.encode64(encryptedChunk)); - } - - return encryptedChunks; - } catch (error) { - return []; - } -} - -// Decrypt message with private key -function decrypt(encryptedMessages: string[], privateKey): string { - const privateKeyObj = forge.pki.privateKeyFromPem(privateKey); - let decryptedMessages: string[] = []; - - encryptedMessages.forEach((encryptedMessage) => { - const encrypted = forge.util.decode64(encryptedMessage); - const decrypted = privateKeyObj.decrypt(encrypted, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - decryptedMessages.push(decrypted); - }); - - return decryptedMessages.join(''); -} - -// Chat Bar Icon Component -const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { - [enabled, setEnabled] = useState(false); - - useEffect(() => { - const listener: SendListener = async (_, message) => { - if (enabled) { - const groupChannel = await DataStore.get('encryptcordChannelId'); - if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` }); - message.content = ""; - return; - } - const trimmedMessage = message.content.trim(); - await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0)); - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - const encryptedMessage = encrypt(trimmedMessage, encryptcordGroupMembers[memberId]); - const encryptedMessageString = JSON.stringify(encryptedMessage); - await sendTempMessage(groupMember.id, encryptedMessageString, `message`); - }); - - await Promise.all(dmPromises); - message.content = ""; - } - }; - - addPreSendListener(listener); - return () => void removePreSendListener(listener); - }, [enabled]); - - if (!isMainChat) return null; - - return ( - { - const groupChannel = await DataStore.get('encryptcordChannelId'); - if (await DataStore.get('encryptcordGroup') == false) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in an E2EE group to send an encrypted message!` }); - return; - } - if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in the E2EE group channel to send an encrypted message!` }); - return; - } - setEnabled(!enabled); - }} - buttonProps={{ - "aria-haspopup": "dialog", - }} - > - - {!enabled && <> - - - - - } - - - - ); -}; - -// Export Plugin -export default definePlugin({ - name: "Encryptcord", - description: "End-to-end encryption in Discord!", - authors: [Devs.Inbestigator], - dependencies: ["CommandsAPI"], - patches: [ - { - find: "executeMessageComponentInteraction:", - replacement: { - match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, - replace: 'if(await $self.joinGroup(C))return;$&' - } - } - ], - async joinGroup(interaction) { - const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); - if (!sender || sender.bot == true) return false; - if (interaction.data.component_type == 2 && interaction.data.custom_id == "acceptGroup") { - await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); - } - return true; - }, - flux: { - async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (!message.content) return; - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - if (!encryptcordGroupJoinList.includes(message.author.id)) { - switch (message.content.split("/")[0].toLowerCase()) { - case "e2eeinvite": - const inviteMessage = `I've invited you to an [end-to-end encrypted]() group in <#${message.content.split("/")[1]}>.`; - await MessageActions.receiveMessage(channelId, { - ...await createMessage(inviteMessage, message.author.id, channelId, 0), components: [{ - type: 1, - components: [{ - type: 2, - style: 3, - label: 'Accept!', - custom_id: 'acceptGroup' - }] - }] - }); - break; - case "groupdata": - const groupdata = (await axios.get(message.attachments[0].url)).data; - await handleGroupData(groupdata); - break; - default: - break; - } - return; - }; - if (message.content.toLowerCase() !== "join") return; - const sender = await UserUtils.getUser(message.author.id).catch(() => null); - if (!sender) return; - const userKey = (await axios.get(message.attachments[0].url)).data; - await handleJoin(sender.id, userKey, encryptcordGroupMembers); - return; - } - const dmChannelId = await RestAPI.post({ - url: `/users/@me/channels`, - body: { - recipient_id: message.author.id, - }, - }).then((response) => response.body.id); - if (channelId !== dmChannelId) return; - const sender = await UserUtils.getUser(message.author.id).catch(() => null); - if (!sender) return; - const groupChannel = await DataStore.get('encryptcordChannelId'); - switch (message.content.toLowerCase()) { - case "leaving": - handleLeaving(sender.id, encryptcordGroupMembers, groupChannel); - break; - case "message": - const messagedata = (await axios.get(message.attachments[0].url)).data; - await handleMessage(messagedata, sender.id, groupChannel); - break; - case "groupdata": - const groupdata = (await axios.get(message.attachments[0].url)).data; - await handleGroupData(groupdata); - break; - default: - break; - } - }, - }, - commands: [ - { - name: "encryptcord", - description: "End-to-end encryption in Discord!", - options: [ - { - name: "leave", - description: "Leave current group", - options: [], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - { - name: "start", - description: "Start an E2EE group", - options: [], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - { - name: "invite", - description: "Invite a user to your group", - options: [ - { - name: "user", - description: "Who to invite", - required: true, - type: ApplicationCommandOptionType.USER, - }, - ], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - ], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - switch (opts[0].name) { - case "start": - startGroup(opts[0].options, ctx); - break; - case "invite": - invite(opts[0].options, ctx); - break; - case "leave": - leave(opts[0].options, ctx); - break; - } - }, - }, - ], - startAt: StartAt.DOMContentLoaded, - async start() { - addChatBarButton("Encryptcord", ChatBarIcon); - const pair = generateKeyPair(); - await DataStore.set('encryptcordPublicKey', pair.publicKey); - await DataStore.set('encryptcordPrivateKey', pair.privateKey); - if (await DataStore.get("encryptcordGroup") == true) { - await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); - } - await DataStore.set('encryptcordGroup', false); - await DataStore.set('encryptcordChannelId', ""); - await DataStore.set('encryptcordGroupMembers', {}); - await DataStore.set('encryptcordGroupJoinList', []); - }, - stop() { - removeButton("Encryptcord"); - }, -}); - -// Send Temporary Message -async function sendTempMessage(recipientId: string, attachment: string, content: string) { - if (recipientId == UserStore.getCurrentUser().id) return; - - const dmChannelId = await RestAPI.post({ - url: `/users/@me/channels`, - body: { - recipient_id: recipientId, - }, - }).then((response) => response.body.id); - - if (attachment && attachment != "") { - const upload = await new CloudUtils.CloudUpload({ - file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }), - isClip: false, - isThumbnail: false, - platform: 1, - }, dmChannelId, false, 0); - upload.on("complete", async () => { - const messageId = await RestAPI.post({ - url: `/channels/${dmChannelId}/messages`, - body: { - content, - attachments: [{ - id: "0", - filename: upload.filename, - uploaded_filename: upload.uploadedFilename, - }], - nonce: SnowflakeUtils.fromTimestamp(Date.now()), - }, - }).then((response) => response.body.id); - - await sleep(500); - RestAPI.delete({ - url: `/channels/${dmChannelId}/messages/${messageId}` - }); - }); - await upload.upload(); - return; - } - - const messageId = await RestAPI.post({ - url: `/channels/${dmChannelId}/messages`, - body: { - content, - nonce: SnowflakeUtils.fromTimestamp(Date.now()), - }, - }).then((response) => response.body.id); - - await sleep(500); - RestAPI.delete({ - url: `/channels/${dmChannelId}/messages/${messageId}` - }); -} - -// Handle leaving group -async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) { - const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => { - if (memberId !== senderId) { - result[memberId] = encryptcordGroupMembers[memberId]; - } - return result; - }, {}); - - await DataStore.set('encryptcordGroupMembers', updatedMembers); - - await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2)); -} - -// Handle receiving message -async function handleMessage(message, senderId: string, groupChannel: string) { - const decryptedMessage = decrypt(message, await DataStore.get("encryptcordPrivateKey")); - await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); -} - -// Handle receiving group data -async function handleGroupData(groupData) { - await DataStore.set('encryptcordChannelId', groupData.channel); - await DataStore.set('encryptcordGroupMembers', groupData.members); - await DataStore.set('encryptcordGroup', true); - await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7)); - setEnabled(true); -} - -// Handle joining group -async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) { - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - const updatedMembers = encryptcordGroupJoinList.filter(memberId => memberId !== senderId); - await DataStore.set('encryptcordGroupJoinList', updatedMembers); - - encryptcordGroupMembers[senderId] = senderKey; - await DataStore.set('encryptcordGroupMembers', encryptcordGroupMembers); - const groupChannel = await DataStore.get('encryptcordChannelId'); - const newMember = await UserUtils.getUser(senderId).catch(() => null); - if (!newMember) return; - - const membersData = {}; - Object.entries(encryptcordGroupMembers) - .forEach(([memberId, value]) => { - membersData[memberId] = value; - }); - - const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel }); - - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - await sendTempMessage(groupMember.id, membersDataString, `groupdata`); - }); - - await Promise.all(dmPromises); - await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 7)); -} - -// Create message for group -async function createMessage(message: string, senderId: string, channelId: string, type: number) { - const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); - const sender = await UserUtils.getUser(senderId).catch(() => null); - if (!sender) return; - return { ...messageStart, content: message, author: sender, type, flags: 0 }; -} - -// Start E2EE Group -async function startGroup(opts, ctx) { - const channelId = ctx.channel.id; - await DataStore.set('encryptcordChannelId', channelId); - await DataStore.set('encryptcordGroupMembers', { - [UserStore.getCurrentUser().id]: await DataStore.get("encryptcordPublicKey") - }); - await DataStore.set('encryptcordGroupJoinList', []); - await DataStore.set('encryptcordGroup', true); - sendBotMessage(channelId, { content: "Group created!" }); - await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); - setEnabled(true); -} - -// Invite User to Group -async function invite(opts, ctx) { - const invitedUser = await UserUtils.getUser(findOption(opts, "user", "")).catch(() => null); - if (!invitedUser) return; - - const channelId = ctx.channel.id; - if (!(await DataStore.get('encryptcordGroup'))) { - sendBotMessage(channelId, { content: `You're not in a group!` }); - return; - } - - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - if (Object.keys(encryptcordGroupMembers).some(key => key == invitedUser.id)) { - sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the group.` }); - return; - } - - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - if (encryptcordGroupJoinList.includes(invitedUser.id)) { - sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the join list.` }); - return; - } - - encryptcordGroupJoinList.push(invitedUser.id); - await DataStore.set('encryptcordGroupJoinList', encryptcordGroupJoinList); - - await sendTempMessage(invitedUser.id, "", `e2eeinvite/${await DataStore.get('encryptcordChannelId')}`); - - sendBotMessage(channelId, { content: `<@${invitedUser.id}> invited successfully.` }); -} - -// Leave the Group -async function leave(opts, ctx) { - const channelId = ctx.channel.id; - if (!(await DataStore.get('encryptcordGroup'))) { - sendBotMessage(channelId, { content: `You're not in a group!` }); - return; - } - const user = UserStore.getCurrentUser(); - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - await sendTempMessage(groupMember.id, "", `leaving`); - }); - - await Promise.all(dmPromises); - await DataStore.set('encryptcordGroup', false); - await DataStore.set('encryptcordChannelId', ""); - await DataStore.set('encryptcordGroupMembers', {}); - await DataStore.set('encryptcordGroupJoinList', []); - await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); - setEnabled(false); -} From faf76d1bbbc769de1c59be6c4f1f8c5ba4f35228 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 29 Feb 2024 07:13:29 -0800 Subject: [PATCH 07/58] Made keys longer --- src/plugins/encryptcord/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index da943a9e28..fdf3652452 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -41,7 +41,7 @@ interface IMessageCreate { // Generate RSA key pair function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 1024 }); + const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); const privateKey = forge.pki.privateKeyToPem(keys.privateKey); const publicKey = forge.pki.publicKeyToPem(keys.publicKey); @@ -52,7 +52,7 @@ function generateKeyPair(): { privateKey: string; publicKey: string; } { function encrypt(message: string, publicKey): string[] { try { const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 62; + const chunkSize = 190; const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; message = message.replace(emojiRegex, ''); From 352b6fc11bd5631c4609d1780cabde62bd042e48 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:21:05 -0800 Subject: [PATCH 08/58] Made keys longer --- src/plugins/encryptcord/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index da943a9e28..fdf3652452 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -41,7 +41,7 @@ interface IMessageCreate { // Generate RSA key pair function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 1024 }); + const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); const privateKey = forge.pki.privateKeyToPem(keys.privateKey); const publicKey = forge.pki.publicKeyToPem(keys.publicKey); @@ -52,7 +52,7 @@ function generateKeyPair(): { privateKey: string; publicKey: string; } { function encrypt(message: string, publicKey): string[] { try { const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 62; + const chunkSize = 190; const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; message = message.replace(emojiRegex, ''); From 3275c01a8b7b918eba775a5762b4e9aa6332c00b Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 29 Feb 2024 21:46:11 -0800 Subject: [PATCH 09/58] Removed axios --- package.json | 1 - pnpm-lock.yaml | 61 +------------------------------ src/plugins/encryptcord/index.tsx | 46 +++++++++++++---------- 3 files changed, 28 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 13017b7925..908a87d62f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@sapphi-red/web-noise-suppressor": "0.3.3", "@vap/core": "0.0.12", "@vap/shiki": "0.10.5", - "axios": "^1.6.7", "eslint-plugin-simple-header": "^1.0.2", "fflate": "^0.7.4", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0cc197b92..6e407334ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,9 +22,6 @@ dependencies: '@vap/shiki': specifier: 0.10.5 version: 0.10.5 - axios: - specifier: ^1.6.7 - version: 1.6.7 eslint-plugin-simple-header: specifier: ^1.0.2 version: 1.0.2 @@ -902,26 +899,12 @@ packages: resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} dev: true - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - /atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} hasBin: true dev: true - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - dev: false - /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1100,13 +1083,6 @@ packages: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} dev: true - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: false - /component-emitter@1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true @@ -1237,11 +1213,6 @@ packages: isobject: 3.0.1 dev: true - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false - /devtools-protocol@0.0.1107588: resolution: {integrity: sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==} dev: true @@ -1835,30 +1806,11 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} dev: true - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - /fragment-cache@0.2.1: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} @@ -2428,18 +2380,6 @@ packages: picomatch: 2.3.1 dev: true - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2759,6 +2699,7 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: true /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index fdf3652452..8d71130b91 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -5,7 +5,7 @@ import * as DataStore from "@api/DataStore"; import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; -import { useEffect, useState } from "@webpack/common"; +import { useEffect, useState, FluxDispatcher } from "@webpack/common"; import { Devs } from "@utils/constants"; import { RestAPI, @@ -23,7 +23,6 @@ import { import { Message } from "discord-types/general"; const MessageCreator = findByPropsLazy("createBotMessage"); const CloudUtils = findByPropsLazy("CloudUpload"); -import axios from 'axios'; import { getCurrentChannel } from "@utils/discord"; import forge from 'node-forge'; @@ -130,17 +129,18 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { onClick={async () => { const groupChannel = await DataStore.get('encryptcordChannelId'); if (await DataStore.get('encryptcordGroup') == false) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in an E2EE group to send an encrypted message!` }); - return; - } - if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in the E2EE group channel to send an encrypted message!` }); + await startGroup("", { channel: { id: getCurrentChannel().id } }); + } else if (getCurrentChannel().id !== groupChannel) { + sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!`, author: { username: "false" } }); return; } setEnabled(!enabled); }} buttonProps={{ - "aria-haspopup": "dialog", + style: { + transition: 'transform 0.3s ease-in-out', + transform: `rotate(${enabled ? 0 : 15}deg)`, + } }} > \s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, - replace: 'if(await $self.joinGroup(C))return;$&' + replace: 'await $self.joinGroup(C);$&' } } ], async joinGroup(interaction) { const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); - if (!sender || sender.bot == true) return false; - if (interaction.data.component_type == 2 && interaction.data.custom_id == "acceptGroup") { - await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); - } - return true; + if (!sender || sender.bot == true) return; + if (interaction.data.component_type != 2 || interaction.data.custom_id != "acceptGroup") return; + await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: interaction.channel_id, + id: interaction.message_id, + mlDeleted: true + }); }, flux: { async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { @@ -215,7 +219,8 @@ export default definePlugin({ }); break; case "groupdata": - const groupdata = (await axios.get(message.attachments[0].url)).data; + const response = await fetch(message.attachments[0].url); + const groupdata = await response.json(); await handleGroupData(groupdata); break; default: @@ -226,7 +231,9 @@ export default definePlugin({ if (message.content.toLowerCase() !== "join") return; const sender = await UserUtils.getUser(message.author.id).catch(() => null); if (!sender) return; - const userKey = (await axios.get(message.attachments[0].url)).data; + const response = await fetch(message.attachments[0].url); + console.log(response); + const userKey = await response.text(); await handleJoin(sender.id, userKey, encryptcordGroupMembers); return; } @@ -245,11 +252,13 @@ export default definePlugin({ handleLeaving(sender.id, encryptcordGroupMembers, groupChannel); break; case "message": - const messagedata = (await axios.get(message.attachments[0].url)).data; + const msgResponse = await fetch(message.attachments[0].url); + const messagedata = await msgResponse.json(); await handleMessage(messagedata, sender.id, groupChannel); break; case "groupdata": - const groupdata = (await axios.get(message.attachments[0].url)).data; + const response = await fetch(message.attachments[0].url); + const groupdata = await response.json(); await handleGroupData(groupdata); break; default: @@ -304,7 +313,6 @@ export default definePlugin({ }, }, ], - startAt: StartAt.DOMContentLoaded, async start() { addChatBarButton("Encryptcord", ChatBarIcon); const pair = generateKeyPair(); From 92287026daf988a8fb2af58d91ba68c492ca6e6f Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 02:41:04 -0800 Subject: [PATCH 10/58] Removed node-forge, encryptcord is now fully self contained --- package.json | 1 - pnpm-lock.yaml | 8 -- src/plugins/encryptcord/index.tsx | 111 +++++++++++------------- src/plugins/encryptcord/rsa-utils.tsx | 116 ++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 73 deletions(-) create mode 100644 src/plugins/encryptcord/rsa-utils.tsx diff --git a/package.json b/package.json index 908a87d62f..91e596358f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "monaco-editor": "^0.43.0", "nanoid": "^4.0.2", - "node-forge": "^1.3.1", "virtual-merge": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e407334ca..55799a516a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,9 +37,6 @@ dependencies: nanoid: specifier: ^4.0.2 version: 4.0.2 - node-forge: - specifier: ^1.3.1 - version: 1.3.1 virtual-merge: specifier: ^1.0.1 version: 1.0.1 @@ -2483,11 +2480,6 @@ packages: whatwg-url: 5.0.0 dev: true - /node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - dev: false - /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index 8d71130b91..d60f7d0ca7 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -1,11 +1,13 @@ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { removeButton } from "@api/MessagePopover"; -import definePlugin, { StartAt } from "@utils/types"; +import { addDecoration } from "@api/MessageDecorations"; +import definePlugin from "@utils/types"; import * as DataStore from "@api/DataStore"; import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { useEffect, useState, FluxDispatcher } from "@webpack/common"; +import { generateKeys, encryptData, decryptData } from "./rsa-utils"; import { Devs } from "@utils/constants"; import { RestAPI, @@ -38,56 +40,6 @@ interface IMessageCreate { message: Message; } -// Generate RSA key pair -function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); - const privateKey = forge.pki.privateKeyToPem(keys.privateKey); - const publicKey = forge.pki.publicKeyToPem(keys.publicKey); - - return { privateKey, publicKey }; -} - -// Encrypt message with public key -function encrypt(message: string, publicKey): string[] { - try { - const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 190; - - const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; - message = message.replace(emojiRegex, ''); - - const encryptedChunks: string[] = []; - - for (let i = 0; i < message.length; i += chunkSize) { - const chunk = message.substring(i, i + chunkSize); - const encryptedChunk = publicKeyObj.encrypt(chunk, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - encryptedChunks.push(forge.util.encode64(encryptedChunk)); - } - - return encryptedChunks; - } catch (error) { - return []; - } -} - -// Decrypt message with private key -function decrypt(encryptedMessages: string[], privateKey): string { - const privateKeyObj = forge.pki.privateKeyFromPem(privateKey); - let decryptedMessages: string[] = []; - - encryptedMessages.forEach((encryptedMessage) => { - const encrypted = forge.util.decode64(encryptedMessage); - const decrypted = privateKeyObj.decrypt(encrypted, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - decryptedMessages.push(decrypted); - }); - - return decryptedMessages.join(''); -} - // Chat Bar Icon Component const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { [enabled, setEnabled] = useState(false); @@ -107,7 +59,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { const groupMember = await UserUtils.getUser(memberId).catch(() => null); if (!groupMember) return; - const encryptedMessage = encrypt(trimmedMessage, encryptcordGroupMembers[memberId]); + const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId], trimmedMessage); const encryptedMessageString = JSON.stringify(encryptedMessage); await sendTempMessage(groupMember.id, encryptedMessageString, `message`); }); @@ -185,14 +137,29 @@ export default definePlugin({ async joinGroup(interaction) { const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); if (!sender || sender.bot == true) return; - if (interaction.data.component_type != 2 || interaction.data.custom_id != "acceptGroup") return; - await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: interaction.channel_id, - id: interaction.message_id, - mlDeleted: true - }); + if (interaction.data.component_type != 2) return; + switch (interaction.data.custom_id) { + case "acceptGroup": + await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: interaction.channel_id, + id: interaction.message_id, + mlDeleted: true + }); + break; + case "removeFromGroup": + await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id); + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: interaction.channel_id, + id: interaction.message_id, + mlDeleted: true + }); + break; + default: + return; + } }, flux: { async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { @@ -232,7 +199,6 @@ export default definePlugin({ const sender = await UserUtils.getUser(message.author.id).catch(() => null); if (!sender) return; const response = await fetch(message.attachments[0].url); - console.log(response); const userKey = await response.text(); await handleJoin(sender.id, userKey, encryptcordGroupMembers); return; @@ -315,7 +281,7 @@ export default definePlugin({ ], async start() { addChatBarButton("Encryptcord", ChatBarIcon); - const pair = generateKeyPair(); + const pair = await generateKeys(); await DataStore.set('encryptcordPublicKey', pair.publicKey); await DataStore.set('encryptcordPrivateKey', pair.privateKey); if (await DataStore.get("encryptcordGroup") == true) { @@ -402,7 +368,7 @@ async function handleLeaving(senderId: string, encryptcordGroupMembers: object, // Handle receiving message async function handleMessage(message, senderId: string, groupChannel: string) { - const decryptedMessage = decrypt(message, await DataStore.get("encryptcordPrivateKey")); + const decryptedMessage = await decryptData(await DataStore.get("encryptcordPrivateKey"), message); await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); } @@ -442,7 +408,24 @@ async function handleJoin(senderId: string, senderKey: string, encryptcordGroupM }); await Promise.all(dmPromises); - await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 7)); + await MessageActions.receiveMessage(groupChannel, { + ...await createMessage("", senderId, groupChannel, 7), components: [{ + type: 1, + components: [{ + type: 2, + style: 4, + label: 'I don\'t want to talk to you!', + custom_id: 'removeFromGroup' + }, + { + type: 2, + style: 2, + label: '(Other users can still send/receive messages to/from them)', + disabled: true, + custom_id: 'encryptcord' + }] + }] + }); } // Create message for group diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx new file mode 100644 index 0000000000..9d06256bad --- /dev/null +++ b/src/plugins/encryptcord/rsa-utils.tsx @@ -0,0 +1,116 @@ +export const generateKeys = async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"] + ); + + const exportedPublicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); + const publicKey = formatPemKey(exportedPublicKey); + + return { privateKey: keyPair.privateKey, publicKey }; +}; + +export const encryptData = async (pemPublicKey, data) => { + const publicKey = await importPemPublicKey(pemPublicKey); + + const chunkSize = 190; + + const encryptedChunks: any[] = []; + const encoder = new TextEncoder(); + + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = await data.substring(i, i + chunkSize); + const encryptedChunk = await crypto.subtle.encrypt( + { + name: "RSA-OAEP", + }, + publicKey, + encoder.encode(chunk) + ); + encryptedChunks.push(arrayBufferToBase64(encryptedChunk)); + } + + return encryptedChunks; +}; + +export const decryptData = async (privateKey, encArray) => { + const decryptionPromises = encArray.map(async (encStr) => { + const encBuffer = base64ToArrayBuffer(encStr); + + const dec = await crypto.subtle.decrypt( + { + name: "RSA-OAEP", + }, + privateKey, + encBuffer + ); + + return new TextDecoder().decode(dec); + }); + + const decryptedMessages = await Promise.all(decryptionPromises); + + return decryptedMessages.join(''); +}; + +// Helper functions +const arrayBufferToBase64 = (buffer) => { + const binary = String.fromCharCode(...new Uint8Array(buffer)); + return btoa(binary); +}; + +const base64ToArrayBuffer = (base64String) => { + const binaryString = atob(base64String); + const length = binaryString.length; + const buffer = new ArrayBuffer(length); + const view = new Uint8Array(buffer); + + for (let i = 0; i < length; i++) { + view[i] = binaryString.charCodeAt(i); + } + + return buffer; +}; + +const formatPemKey = (keyData) => { + const base64Key = arrayBufferToBase64(keyData); + return `-----BEGIN PUBLIC KEY-----\n` + base64Key + `\n----- END PUBLIC KEY----- `; +}; + +const importPemPublicKey = async (pemKey) => { + try { + const trimmedPemKey = pemKey.trim(); + + const keyBody = trimmedPemKey + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("----- END PUBLIC KEY-----", ""); + + const binaryDer = atob(keyBody); + + const arrayBuffer = new Uint8Array(binaryDer.length); + for (let i = 0; i < binaryDer.length; i++) { + arrayBuffer[i] = binaryDer.charCodeAt(i); + } + + return await crypto.subtle.importKey( + "spki", + arrayBuffer, + { + name: "RSA-OAEP", + hash: { name: "SHA-256" }, + }, + true, + ["encrypt"] + ); + } catch (error) { + console.error("Error importing PEM public key:", error); + throw error; + } +}; + From a7b7008d88218f4497de6f96b8b294626b89f8ee Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 02:48:45 -0800 Subject: [PATCH 11/58] Minor patches Thinking of overhauling the join stuff tomorrow --- src/plugins/encryptcord/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index d60f7d0ca7..cca5e2e6a0 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -148,8 +148,9 @@ export default definePlugin({ mlDeleted: true }); break; - case "removeFromGroup": + case "removeFromSelf": await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id); + await sendTempMessage(sender.id, "", "leaving"); FluxDispatcher.dispatch({ type: "MESSAGE_DELETE", channelId: interaction.channel_id, @@ -415,7 +416,7 @@ async function handleJoin(senderId: string, senderKey: string, encryptcordGroupM type: 2, style: 4, label: 'I don\'t want to talk to you!', - custom_id: 'removeFromGroup' + custom_id: 'removeFromSelf' }, { type: 2, @@ -445,7 +446,7 @@ async function startGroup(opts, ctx) { }); await DataStore.set('encryptcordGroupJoinList', []); await DataStore.set('encryptcordGroup', true); - sendBotMessage(channelId, { content: "Group created!" }); + sendBotMessage(channelId, { content: "Group created!\n> You can do `/encryptcord invite` to invite other users to this group." }); await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); setEnabled(true); } From 0ed92fdc170e21a360ce691069c90e0c0243e166 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 03:17:57 -0800 Subject: [PATCH 12/58] Raised key size and added creategroup button --- src/plugins/encryptcord/index.tsx | 19 ++++++++++++++++--- src/plugins/encryptcord/rsa-utils.tsx | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index cca5e2e6a0..2ef5096a74 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -26,7 +26,6 @@ import { Message } from "discord-types/general"; const MessageCreator = findByPropsLazy("createBotMessage"); const CloudUtils = findByPropsLazy("CloudUpload"); import { getCurrentChannel } from "@utils/discord"; -import forge from 'node-forge'; let enabled; let setEnabled; @@ -83,7 +82,17 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { if (await DataStore.get('encryptcordGroup') == false) { await startGroup("", { channel: { id: getCurrentChannel().id } }); } else if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!`, author: { username: "false" } }); + sendBotMessage(getCurrentChannel().id, { + content: `You are already in an encrypted group in <#${groupChannel}>.\n> Would you like to create a new one?`, components: [{ + type: 1, + components: [{ + type: 2, + style: 3, + label: 'Yes', + custom_id: 'createGroup' + }] + }] + }); return; } setEnabled(!enabled); @@ -136,7 +145,7 @@ export default definePlugin({ ], async joinGroup(interaction) { const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); - if (!sender || sender.bot == true) return; + if (!sender || (sender.bot == true && sender.id != "1")) return; if (interaction.data.component_type != 2) return; switch (interaction.data.custom_id) { case "acceptGroup": @@ -158,6 +167,10 @@ export default definePlugin({ mlDeleted: true }); break; + case "createGroup": + await leave("", { channel: { id: interaction.channel_id } }); + await startGroup("", { channel: { id: interaction.channel_id } }); + break; default: return; } diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx index 9d06256bad..219d76d677 100644 --- a/src/plugins/encryptcord/rsa-utils.tsx +++ b/src/plugins/encryptcord/rsa-utils.tsx @@ -2,7 +2,7 @@ export const generateKeys = async () => { const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", - modulusLength: 2048, + modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, @@ -19,7 +19,7 @@ export const generateKeys = async () => { export const encryptData = async (pemPublicKey, data) => { const publicKey = await importPemPublicKey(pemPublicKey); - const chunkSize = 190; + const chunkSize = 446; const encryptedChunks: any[] = []; const encoder = new TextEncoder(); From fb039f9adca5a14018b1d28788b2ec2f22a5fc3c Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 12:14:47 -0800 Subject: [PATCH 13/58] Added data command --- src/plugins/encryptcord/index.tsx | 26 ++++++++++++++++++++++++-- src/plugins/encryptcord/rsa-utils.tsx | 6 +++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index 2ef5096a74..ce1e85c147 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -1,13 +1,12 @@ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { removeButton } from "@api/MessagePopover"; -import { addDecoration } from "@api/MessageDecorations"; import definePlugin from "@utils/types"; import * as DataStore from "@api/DataStore"; import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { useEffect, useState, FluxDispatcher } from "@webpack/common"; -import { generateKeys, encryptData, decryptData } from "./rsa-utils"; +import { generateKeys, encryptData, decryptData, formatPemKey } from "./rsa-utils"; import { Devs } from "@utils/constants"; import { RestAPI, @@ -276,6 +275,12 @@ export default definePlugin({ ], type: ApplicationCommandOptionType.SUB_COMMAND, }, + { + name: "data", + description: "View your keys and current group members", + options: [], + type: ApplicationCommandOptionType.SUB_COMMAND, + }, ], inputType: ApplicationCommandInputType.BOT, execute: (opts, ctx) => { @@ -289,6 +294,9 @@ export default definePlugin({ case "leave": leave(opts[0].options, ctx); break; + case "data": + data(opts[0].options, ctx); + break; } }, }, @@ -519,3 +527,17 @@ async function leave(opts, ctx) { await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); setEnabled(false); } + +// View user data +async function data(opts, ctx) { + const channelId = ctx.channel.id; + const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); + const encryptcordPublicKey = await DataStore.get('encryptcordPublicKey'); + const encryptcordPrivateKey = await DataStore.get('encryptcordPrivateKey'); + const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", encryptcordPrivateKey); + const groupMembers = Object.keys(encryptcordGroupMembers); + sendBotMessage(channelId, { + content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\`` + }); +} + diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx index 219d76d677..9616fcf98e 100644 --- a/src/plugins/encryptcord/rsa-utils.tsx +++ b/src/plugins/encryptcord/rsa-utils.tsx @@ -11,7 +11,7 @@ export const generateKeys = async () => { ); const exportedPublicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); - const publicKey = formatPemKey(exportedPublicKey); + const publicKey = formatPemKey(exportedPublicKey, "public"); return { privateKey: keyPair.privateKey, publicKey }; }; @@ -78,9 +78,9 @@ const base64ToArrayBuffer = (base64String) => { return buffer; }; -const formatPemKey = (keyData) => { +export const formatPemKey = (keyData, type) => { const base64Key = arrayBufferToBase64(keyData); - return `-----BEGIN PUBLIC KEY-----\n` + base64Key + `\n----- END PUBLIC KEY----- `; + return `-----BEGIN ${type.toUpperCase()} KEY-----\n` + base64Key + `\n----- END ${type.toUpperCase()} KEY----- `; }; const importPemPublicKey = async (pemKey) => { From 9d6d8940b1c4257e2b502dcee2663b5ad56d9ade Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 17:08:06 -0800 Subject: [PATCH 14/58] Joining overhaul --- src/plugins/encryptcord/index.tsx | 176 +++++++++--------------------- 1 file changed, 49 insertions(+), 127 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index ce1e85c147..bd440e0b4a 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -57,7 +57,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { const groupMember = await UserUtils.getUser(memberId).catch(() => null); if (!groupMember) return; - const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId], trimmedMessage); + const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId].key, trimmedMessage); const encryptedMessageString = JSON.stringify(encryptedMessage); await sendTempMessage(groupMember.id, encryptedMessageString, `message`); }); @@ -77,22 +77,17 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { { - const groupChannel = await DataStore.get('encryptcordChannelId'); - if (await DataStore.get('encryptcordGroup') == false) { + if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { + await sendTempMessage(getCurrentChannel().id, `${await DataStore.get("encryptcordPublicKey")}`, "join", false); + sendBotMessage(getCurrentChannel().id, { content: "*Checking for any groups in this channel...*" }); + await sleep(5000); + if (await DataStore.get('encryptcordGroup') == true && (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { + sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); + await leave("", { channel: { id: await DataStore.get('encryptcordChannelId') } }); + } else { + return; + }; await startGroup("", { channel: { id: getCurrentChannel().id } }); - } else if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { - content: `You are already in an encrypted group in <#${groupChannel}>.\n> Would you like to create a new one?`, components: [{ - type: 1, - components: [{ - type: 2, - style: 3, - label: 'Yes', - custom_id: 'createGroup' - }] - }] - }); - return; } setEnabled(!enabled); }} @@ -147,15 +142,6 @@ export default definePlugin({ if (!sender || (sender.bot == true && sender.id != "1")) return; if (interaction.data.component_type != 2) return; switch (interaction.data.custom_id) { - case "acceptGroup": - await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: interaction.channel_id, - id: interaction.message_id, - mlDeleted: true - }); - break; case "removeFromSelf": await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id); await sendTempMessage(sender.id, "", "leaving"); @@ -178,42 +164,28 @@ export default definePlugin({ async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { if (optimistic || type !== "MESSAGE_CREATE") return; if (message.state === "SENDING") return; + if (message.author.id == UserStore.getCurrentUser().id) return; if (!message.content) return; const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - if (!encryptcordGroupJoinList.includes(message.author.id)) { - switch (message.content.split("/")[0].toLowerCase()) { - case "e2eeinvite": - const inviteMessage = `I've invited you to an [end-to-end encrypted]() group in <#${message.content.split("/")[1]}>.`; - await MessageActions.receiveMessage(channelId, { - ...await createMessage(inviteMessage, message.author.id, channelId, 0), components: [{ - type: 1, - components: [{ - type: 2, - style: 3, - label: 'Accept!', - custom_id: 'acceptGroup' - }] - }] - }); - break; - case "groupdata": - const response = await fetch(message.attachments[0].url); - const groupdata = await response.json(); - await handleGroupData(groupdata); - break; - default: - break; - } - return; - }; - if (message.content.toLowerCase() !== "join") return; - const sender = await UserUtils.getUser(message.author.id).catch(() => null); - if (!sender) return; - const response = await fetch(message.attachments[0].url); - const userKey = await response.text(); - await handleJoin(sender.id, userKey, encryptcordGroupMembers); + switch (message.content.toLowerCase()) { + case "groupdata": + const response = await fetch(message.attachments[0].url); + const groupdata = await response.json(); + await handleGroupData(groupdata); + break; + case "join": + if (encryptcordGroupMembers[UserStore.getCurrentUser().id].child) return; + if (!await DataStore.get("encryptcordGroup")) return; + const sender = await UserUtils.getUser(message.author.id).catch(() => null); + if (!sender) return; + const joinresponse = await fetch(message.attachments[0].url); + const userKey = await joinresponse.text(); + await handleJoin(sender.id, userKey, encryptcordGroupMembers); + break; + default: + break; + } return; } const dmChannelId = await RestAPI.post({ @@ -256,25 +228,6 @@ export default definePlugin({ options: [], type: ApplicationCommandOptionType.SUB_COMMAND, }, - { - name: "start", - description: "Start an E2EE group", - options: [], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - { - name: "invite", - description: "Invite a user to your group", - options: [ - { - name: "user", - description: "Who to invite", - required: true, - type: ApplicationCommandOptionType.USER, - }, - ], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, { name: "data", description: "View your keys and current group members", @@ -288,9 +241,6 @@ export default definePlugin({ case "start": startGroup(opts[0].options, ctx); break; - case "invite": - invite(opts[0].options, ctx); - break; case "leave": leave(opts[0].options, ctx); break; @@ -312,23 +262,25 @@ export default definePlugin({ await DataStore.set('encryptcordGroup', false); await DataStore.set('encryptcordChannelId', ""); await DataStore.set('encryptcordGroupMembers', {}); - await DataStore.set('encryptcordGroupJoinList', []); }, - stop() { + async stop() { removeButton("Encryptcord"); + if (await DataStore.get("encryptcordGroup") == true) { + await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); + } }, }); // Send Temporary Message -async function sendTempMessage(recipientId: string, attachment: string, content: string) { +async function sendTempMessage(recipientId: string, attachment: string, content: string, dm: boolean = true) { if (recipientId == UserStore.getCurrentUser().id) return; - const dmChannelId = await RestAPI.post({ + const dmChannelId = dm ? await RestAPI.post({ url: `/users/@me/channels`, body: { recipient_id: recipientId, }, - }).then((response) => response.body.id); + }).then((response) => response.body.id) : recipientId; if (attachment && attachment != "") { const upload = await new CloudUtils.CloudUpload({ @@ -379,6 +331,12 @@ async function handleLeaving(senderId: string, encryptcordGroupMembers: object, const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => { if (memberId !== senderId) { result[memberId] = encryptcordGroupMembers[memberId]; + if (result[memberId].child == senderId) { + result[memberId].child = encryptcordGroupMembers[senderId].child; + } + if (result[memberId].parent == senderId) { + result[memberId].parent = encryptcordGroupMembers[senderId].parent; + } } return result; }, {}); @@ -405,11 +363,8 @@ async function handleGroupData(groupData) { // Handle joining group async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) { - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - const updatedMembers = encryptcordGroupJoinList.filter(memberId => memberId !== senderId); - await DataStore.set('encryptcordGroupJoinList', updatedMembers); - - encryptcordGroupMembers[senderId] = senderKey; + encryptcordGroupMembers[senderId] = { key: senderKey, parent: UserStore.getCurrentUser().id, child: null }; + encryptcordGroupMembers[UserStore.getCurrentUser().id].child = senderId; await DataStore.set('encryptcordGroupMembers', encryptcordGroupMembers); const groupChannel = await DataStore.get('encryptcordChannelId'); const newMember = await UserUtils.getUser(senderId).catch(() => null); @@ -463,47 +418,15 @@ async function startGroup(opts, ctx) { const channelId = ctx.channel.id; await DataStore.set('encryptcordChannelId', channelId); await DataStore.set('encryptcordGroupMembers', { - [UserStore.getCurrentUser().id]: await DataStore.get("encryptcordPublicKey") + [UserStore.getCurrentUser().id]: { key: await DataStore.get("encryptcordPublicKey"), parent: null, child: null } }); - await DataStore.set('encryptcordGroupJoinList', []); await DataStore.set('encryptcordGroup', true); - sendBotMessage(channelId, { content: "Group created!\n> You can do `/encryptcord invite` to invite other users to this group." }); + sendBotMessage(channelId, { content: "Group created!\n> Other users can click the lock icon to join." }); await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); setEnabled(true); } -// Invite User to Group -async function invite(opts, ctx) { - const invitedUser = await UserUtils.getUser(findOption(opts, "user", "")).catch(() => null); - if (!invitedUser) return; - - const channelId = ctx.channel.id; - if (!(await DataStore.get('encryptcordGroup'))) { - sendBotMessage(channelId, { content: `You're not in a group!` }); - return; - } - - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - if (Object.keys(encryptcordGroupMembers).some(key => key == invitedUser.id)) { - sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the group.` }); - return; - } - - const encryptcordGroupJoinList = await DataStore.get('encryptcordGroupJoinList'); - if (encryptcordGroupJoinList.includes(invitedUser.id)) { - sendBotMessage(channelId, { content: `<@${invitedUser.id}> is already in the join list.` }); - return; - } - - encryptcordGroupJoinList.push(invitedUser.id); - await DataStore.set('encryptcordGroupJoinList', encryptcordGroupJoinList); - - await sendTempMessage(invitedUser.id, "", `e2eeinvite/${await DataStore.get('encryptcordChannelId')}`); - - sendBotMessage(channelId, { content: `<@${invitedUser.id}> invited successfully.` }); -} - -// Leave the Group +// Leave the Group; async function leave(opts, ctx) { const channelId = ctx.channel.id; if (!(await DataStore.get('encryptcordGroup'))) { @@ -523,7 +446,6 @@ async function leave(opts, ctx) { await DataStore.set('encryptcordGroup', false); await DataStore.set('encryptcordChannelId', ""); await DataStore.set('encryptcordGroupMembers', {}); - await DataStore.set('encryptcordGroupJoinList', []); await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); setEnabled(false); } @@ -537,7 +459,7 @@ async function data(opts, ctx) { const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", encryptcordPrivateKey); const groupMembers = Object.keys(encryptcordGroupMembers); sendBotMessage(channelId, { - content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\`` + content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||*(DO **NOT** SHARE THIS)*\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\`` }); } From 0ffe846cf615f49a65aa60c99fbbcc85b009d3c2 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 22:45:59 -0800 Subject: [PATCH 15/58] Fixed a couple things --- src/plugins/encryptcord/index.tsx | 11 +++++------ src/plugins/encryptcord/rsa-utils.tsx | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index bd440e0b4a..a308f7b82e 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -78,13 +78,13 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { tooltip={enabled ? "Send Unencrypted Messages" : "Send Encrypted Messages"} onClick={async () => { if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { - await sendTempMessage(getCurrentChannel().id, `${await DataStore.get("encryptcordPublicKey")}`, "join", false); - sendBotMessage(getCurrentChannel().id, { content: "*Checking for any groups in this channel...*" }); + await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false); + sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created ` }); await sleep(5000); if (await DataStore.get('encryptcordGroup') == true && (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); await leave("", { channel: { id: await DataStore.get('encryptcordChannelId') } }); - } else { + } else if (await DataStore.get('encryptcordGroup') == true) { return; }; await startGroup("", { channel: { id: getCurrentChannel().id } }); @@ -168,7 +168,7 @@ export default definePlugin({ if (!message.content) return; const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { - switch (message.content.toLowerCase()) { + switch (message.content.toLowerCase().split("```")[0]) { case "groupdata": const response = await fetch(message.attachments[0].url); const groupdata = await response.json(); @@ -179,8 +179,7 @@ export default definePlugin({ if (!await DataStore.get("encryptcordGroup")) return; const sender = await UserUtils.getUser(message.author.id).catch(() => null); if (!sender) return; - const joinresponse = await fetch(message.attachments[0].url); - const userKey = await joinresponse.text(); + const userKey = message.content.split("```")[1]; await handleJoin(sender.id, userKey, encryptcordGroupMembers); break; default: diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx index 9616fcf98e..80e3ca35fa 100644 --- a/src/plugins/encryptcord/rsa-utils.tsx +++ b/src/plugins/encryptcord/rsa-utils.tsx @@ -80,7 +80,7 @@ const base64ToArrayBuffer = (base64String) => { export const formatPemKey = (keyData, type) => { const base64Key = arrayBufferToBase64(keyData); - return `-----BEGIN ${type.toUpperCase()} KEY-----\n` + base64Key + `\n----- END ${type.toUpperCase()} KEY----- `; + return `-----BEGIN ${type.toUpperCase()} KEY-----\n` + base64Key + `\n-----END ${type.toUpperCase()} KEY----- `; }; const importPemPublicKey = async (pemKey) => { @@ -89,7 +89,7 @@ const importPemPublicKey = async (pemKey) => { const keyBody = trimmedPemKey .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("----- END PUBLIC KEY-----", ""); + .replace("-----END PUBLIC KEY-----", ""); const binaryDer = atob(keyBody); From 1237687e07e0044795c889d994c638f8d9372fef Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 3 Mar 2024 18:05:30 -0800 Subject: [PATCH 16/58] Made join button unspammable --- src/plugins/encryptcord/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index a308f7b82e..fcaadcb4d1 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -41,6 +41,7 @@ interface IMessageCreate { // Chat Bar Icon Component const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { [enabled, setEnabled] = useState(false); + let [buttonDisabled, setButtonDisabled] = useState(false); useEffect(() => { const listener: SendListener = async (_, message) => { @@ -78,6 +79,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { tooltip={enabled ? "Send Unencrypted Messages" : "Send Encrypted Messages"} onClick={async () => { if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { + setButtonDisabled(true); await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false); sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created ` }); await sleep(5000); @@ -90,12 +92,14 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { await startGroup("", { channel: { id: getCurrentChannel().id } }); } setEnabled(!enabled); + setButtonDisabled(false); }} buttonProps={{ style: { transition: 'transform 0.3s ease-in-out', transform: `rotate(${enabled ? 0 : 15}deg)`, - } + }, + disabled: buttonDisabled }} > Date: Sun, 3 Mar 2024 18:21:36 -0800 Subject: [PATCH 17/58] Added leave tooltip --- src/plugins/encryptcord/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index fcaadcb4d1..d43707879c 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -81,7 +81,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { setButtonDisabled(true); await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false); - sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created ` }); + sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created \n> You can do \`/encryptcord leave\` to leave the current group` }); await sleep(5000); if (await DataStore.get('encryptcordGroup') == true && (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); From b21f77f32c7ecc68c8f5014b891645982bf75002 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 3 Mar 2024 18:34:45 -0800 Subject: [PATCH 18/58] Removing dependencies I forgot to --- package.json | 1 - pnpm-lock.yaml | 9 --------- 2 files changed, 10 deletions(-) diff --git a/package.json b/package.json index 91e596358f..dde55d3114 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "@types/diff": "^5.0.3", "@types/lodash": "^4.14.194", "@types/node": "^18.16.3", - "@types/node-forge": "^1.3.11", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.1", "@types/yazl": "^2.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55799a516a..4d7ec54182 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,9 +54,6 @@ devDependencies: '@types/node': specifier: ^18.16.3 version: 18.16.3 - '@types/node-forge': - specifier: ^1.3.11 - version: 1.3.11 '@types/react': specifier: ^18.2.0 version: 18.2.0 @@ -579,12 +576,6 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true - /@types/node-forge@1.3.11: - resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - dependencies: - '@types/node': 18.16.3 - dev: true - /@types/node@18.16.3: resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} dev: true From 1297636b86f96f71fd973e1c476d0c4240bc13fc Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 3 Mar 2024 18:43:45 -0800 Subject: [PATCH 19/58] tip --- src/plugins/encryptcord/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index d43707879c..a20296aac6 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -81,7 +81,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { setButtonDisabled(true); await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false); - sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created \n> You can do \`/encryptcord leave\` to leave the current group` }); + sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created \n> [Tip] You can do \`/encryptcord leave\` to leave a group` }); await sleep(5000); if (await DataStore.get('encryptcordGroup') == true && (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); From eecb21fd42c05b524832b834c42940f60d26f41d Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 3 Mar 2024 19:08:20 -0800 Subject: [PATCH 20/58] Fixed privatechannel stuff --- src/plugins/encryptcord/index.tsx | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index a20296aac6..d3e2470266 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -5,7 +5,7 @@ import * as DataStore from "@api/DataStore"; import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; -import { useEffect, useState, FluxDispatcher } from "@webpack/common"; +import { useEffect, useState, FluxDispatcher, PrivateChannelsStore } from "@webpack/common"; import { generateKeys, encryptData, decryptData, formatPemKey } from "./rsa-utils"; import { Devs } from "@utils/constants"; import { @@ -24,7 +24,7 @@ import { import { Message } from "discord-types/general"; const MessageCreator = findByPropsLazy("createBotMessage"); const CloudUtils = findByPropsLazy("CloudUpload"); -import { getCurrentChannel } from "@utils/discord"; +import { getCurrentChannel, openPrivateChannel } from "@utils/discord"; let enabled; let setEnabled; @@ -87,6 +87,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); await leave("", { channel: { id: await DataStore.get('encryptcordChannelId') } }); } else if (await DataStore.get('encryptcordGroup') == true) { + setButtonDisabled(false); return; }; await startGroup("", { channel: { id: getCurrentChannel().id } }); @@ -191,12 +192,7 @@ export default definePlugin({ } return; } - const dmChannelId = await RestAPI.post({ - url: `/users/@me/channels`, - body: { - recipient_id: message.author.id, - }, - }).then((response) => response.body.id); + const dmChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id); if (channelId !== dmChannelId) return; const sender = await UserUtils.getUser(message.author.id).catch(() => null); if (!sender) return; @@ -277,14 +273,7 @@ export default definePlugin({ // Send Temporary Message async function sendTempMessage(recipientId: string, attachment: string, content: string, dm: boolean = true) { if (recipientId == UserStore.getCurrentUser().id) return; - - const dmChannelId = dm ? await RestAPI.post({ - url: `/users/@me/channels`, - body: { - recipient_id: recipientId, - }, - }).then((response) => response.body.id) : recipientId; - + const dmChannelId = dm ? await PrivateChannelsStore.getOrEnsurePrivateChannel(recipientId) : recipientId; if (attachment && attachment != "") { const upload = await new CloudUtils.CloudUpload({ file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }), From 76d7ff03e89b69969fcce7a75b8b44c939895c35 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 02:00:45 -0800 Subject: [PATCH 21/58] Added bypassdnd --- src/plugins/bypassdnd/index.tsx | 155 +++++++++ src/plugins/encryptcord/index.tsx | 457 -------------------------- src/plugins/encryptcord/rsa-utils.tsx | 116 ------- 3 files changed, 155 insertions(+), 573 deletions(-) create mode 100644 src/plugins/bypassdnd/index.tsx delete mode 100644 src/plugins/encryptcord/index.tsx delete mode 100644 src/plugins/encryptcord/rsa-utils.tsx diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx new file mode 100644 index 0000000000..1a826a6e17 --- /dev/null +++ b/src/plugins/bypassdnd/index.tsx @@ -0,0 +1,155 @@ +import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; +import { showNotification } from "@api/Notifications"; +import { definePluginSettings } from "@api/Settings"; +import { DataStore } from "@api/index"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, Menu, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { Channel, Guild, Message, User } from "discord-types/general"; +interface ContextProps { + channel: Channel; + user: User; + guild: Guild; +} + +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + guildId: string; + message: Message; +} + +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { + if (!guild) return; + children.splice(-1, 0, ( + + { + if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); + else bypasses["guilds"].push(guild.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.guilds = (bypasses["guilds"].join(', ')); + }} + /> + + )); +}; + +const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { + if (!channel) return; + children.splice(-1, 0, ( + + { + if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); + else bypasses["channels"].push(channel.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.channels = (bypasses["channels"].join(', ')); + }} + /> + + )); +}; + +const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { + if (!user) return; + children.splice(-1, 0, ( + + { + if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); + else bypasses["users"].push(user.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.users = (bypasses["users"].join(', ')); + }} + /> + + )); +}; + +let bypasses; + +const settings = definePluginSettings({ + guilds: { + type: OptionType.STRING, + description: "Guilds to let bypass (notified when pinged anywhere in guild)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["guild"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + channels: { + type: OptionType.STRING, + description: "Channels to let bypass (notified when pinged in that channel)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + users: { + type: OptionType.STRING, + description: "Users to let bypass (notified for all messages)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + } +}); + +export default definePlugin({ + name: "BypassDND", + description: "Still get notifications from specific sources.", + authors: [Devs.Inbestigator], + flux: { + async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (message.author.id === UserStore.getCurrentUser().id) return; + if (!message.content) return; + + const { guilds, channels, users } = bypasses; + if ((guilds.includes(guildId) || channels.includes(channelId)) && (message.content.includes(`<@${UserStore.getCurrentUser().id}>`) || message.mentions.some(mention => mention.id === UserStore.getCurrentUser().id))) { + await showNotification({ + title: `${message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, + body: message.content, + onClick: () => window.location.href = `https://discord.com/channels/${guildId}/${channelId}/${message.id}` + }); + return; + } + if (users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + await showNotification({ + title: `${message.author.username} sent a message in a DM`, + body: message.content, + onClick: () => window.location.href = `https://discord.com/channels/@me/${channelId}/${message.id}` + }); + } + } + }, + settings, + async start() { + addContextMenuPatch("guild-context", GuildContext); + addContextMenuPatch("channel-context", ChannelContext); + addContextMenuPatch("user-context", UserContext); + bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; + await DataStore.set("bypassdnd", bypasses); + }, + stop() { + removeContextMenuPatch("guild-context", GuildContext); + removeContextMenuPatch("channel-context", ChannelContext); + removeContextMenuPatch("user-context", UserContext); + } +}); diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx deleted file mode 100644 index d3e2470266..0000000000 --- a/src/plugins/encryptcord/index.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; -import { removeButton } from "@api/MessagePopover"; -import definePlugin from "@utils/types"; -import * as DataStore from "@api/DataStore"; -import { sleep } from "@utils/misc"; -import { findByPropsLazy } from "@webpack"; -import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; -import { useEffect, useState, FluxDispatcher, PrivateChannelsStore } from "@webpack/common"; -import { generateKeys, encryptData, decryptData, formatPemKey } from "./rsa-utils"; -import { Devs } from "@utils/constants"; -import { - RestAPI, - SnowflakeUtils, - UserUtils, - UserStore, - MessageActions, -} from "@webpack/common"; -import { - ApplicationCommandInputType, - sendBotMessage, - ApplicationCommandOptionType, - findOption, -} from "@api/Commands"; -import { Message } from "discord-types/general"; -const MessageCreator = findByPropsLazy("createBotMessage"); -const CloudUtils = findByPropsLazy("CloudUpload"); -import { getCurrentChannel, openPrivateChannel } from "@utils/discord"; - -let enabled; -let setEnabled; - -// Interface for Message Create -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - message: Message; -} - -// Chat Bar Icon Component -const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { - [enabled, setEnabled] = useState(false); - let [buttonDisabled, setButtonDisabled] = useState(false); - - useEffect(() => { - const listener: SendListener = async (_, message) => { - if (enabled) { - const groupChannel = await DataStore.get('encryptcordChannelId'); - if (getCurrentChannel().id !== groupChannel) { - sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` }); - message.content = ""; - return; - } - const trimmedMessage = message.content.trim(); - await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0)); - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId].key, trimmedMessage); - const encryptedMessageString = JSON.stringify(encryptedMessage); - await sendTempMessage(groupMember.id, encryptedMessageString, `message`); - }); - - await Promise.all(dmPromises); - message.content = ""; - } - }; - - addPreSendListener(listener); - return () => void removePreSendListener(listener); - }, [enabled]); - - if (!isMainChat) return null; - - return ( - { - if (await DataStore.get('encryptcordGroup') == false || (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { - setButtonDisabled(true); - await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false); - sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created \n> [Tip] You can do \`/encryptcord leave\` to leave a group` }); - await sleep(5000); - if (await DataStore.get('encryptcordGroup') == true && (await DataStore.get('encryptcordChannelId') != getCurrentChannel().id)) { - sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" }); - await leave("", { channel: { id: await DataStore.get('encryptcordChannelId') } }); - } else if (await DataStore.get('encryptcordGroup') == true) { - setButtonDisabled(false); - return; - }; - await startGroup("", { channel: { id: getCurrentChannel().id } }); - } - setEnabled(!enabled); - setButtonDisabled(false); - }} - buttonProps={{ - style: { - transition: 'transform 0.3s ease-in-out', - transform: `rotate(${enabled ? 0 : 15}deg)`, - }, - disabled: buttonDisabled - }} - > - - {!enabled && <> - - - - - } - - - - ); -}; - -// Export Plugin -export default definePlugin({ - name: "Encryptcord", - description: "End-to-end encryption in Discord!", - authors: [Devs.Inbestigator], - dependencies: ["CommandsAPI"], - patches: [ - { - find: "executeMessageComponentInteraction:", - replacement: { - match: /await\s+l\.default\.post\({\s*url:\s*A\.Endpoints\.INTERACTIONS,\s*body:\s*C,\s*timeout:\s*3e3\s*},\s*t\s*=>\s*{\s*h\(T,\s*p,\s*f,\s*t\)\s*}\s*\)/, - replace: 'await $self.joinGroup(C);$&' - } - } - ], - async joinGroup(interaction) { - const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); - if (!sender || (sender.bot == true && sender.id != "1")) return; - if (interaction.data.component_type != 2) return; - switch (interaction.data.custom_id) { - case "removeFromSelf": - await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id); - await sendTempMessage(sender.id, "", "leaving"); - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: interaction.channel_id, - id: interaction.message_id, - mlDeleted: true - }); - break; - case "createGroup": - await leave("", { channel: { id: interaction.channel_id } }); - await startGroup("", { channel: { id: interaction.channel_id } }); - break; - default: - return; - } - }, - flux: { - async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (message.author.id == UserStore.getCurrentUser().id) return; - if (!message.content) return; - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - if (!Object.keys(encryptcordGroupMembers).some(key => key == message.author.id)) { - switch (message.content.toLowerCase().split("```")[0]) { - case "groupdata": - const response = await fetch(message.attachments[0].url); - const groupdata = await response.json(); - await handleGroupData(groupdata); - break; - case "join": - if (encryptcordGroupMembers[UserStore.getCurrentUser().id].child) return; - if (!await DataStore.get("encryptcordGroup")) return; - const sender = await UserUtils.getUser(message.author.id).catch(() => null); - if (!sender) return; - const userKey = message.content.split("```")[1]; - await handleJoin(sender.id, userKey, encryptcordGroupMembers); - break; - default: - break; - } - return; - } - const dmChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id); - if (channelId !== dmChannelId) return; - const sender = await UserUtils.getUser(message.author.id).catch(() => null); - if (!sender) return; - const groupChannel = await DataStore.get('encryptcordChannelId'); - switch (message.content.toLowerCase()) { - case "leaving": - handleLeaving(sender.id, encryptcordGroupMembers, groupChannel); - break; - case "message": - const msgResponse = await fetch(message.attachments[0].url); - const messagedata = await msgResponse.json(); - await handleMessage(messagedata, sender.id, groupChannel); - break; - case "groupdata": - const response = await fetch(message.attachments[0].url); - const groupdata = await response.json(); - await handleGroupData(groupdata); - break; - default: - break; - } - }, - }, - commands: [ - { - name: "encryptcord", - description: "End-to-end encryption in Discord!", - options: [ - { - name: "leave", - description: "Leave current group", - options: [], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - { - name: "data", - description: "View your keys and current group members", - options: [], - type: ApplicationCommandOptionType.SUB_COMMAND, - }, - ], - inputType: ApplicationCommandInputType.BOT, - execute: (opts, ctx) => { - switch (opts[0].name) { - case "start": - startGroup(opts[0].options, ctx); - break; - case "leave": - leave(opts[0].options, ctx); - break; - case "data": - data(opts[0].options, ctx); - break; - } - }, - }, - ], - async start() { - addChatBarButton("Encryptcord", ChatBarIcon); - const pair = await generateKeys(); - await DataStore.set('encryptcordPublicKey', pair.publicKey); - await DataStore.set('encryptcordPrivateKey', pair.privateKey); - if (await DataStore.get("encryptcordGroup") == true) { - await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); - } - await DataStore.set('encryptcordGroup', false); - await DataStore.set('encryptcordChannelId', ""); - await DataStore.set('encryptcordGroupMembers', {}); - }, - async stop() { - removeButton("Encryptcord"); - if (await DataStore.get("encryptcordGroup") == true) { - await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } }); - } - }, -}); - -// Send Temporary Message -async function sendTempMessage(recipientId: string, attachment: string, content: string, dm: boolean = true) { - if (recipientId == UserStore.getCurrentUser().id) return; - const dmChannelId = dm ? await PrivateChannelsStore.getOrEnsurePrivateChannel(recipientId) : recipientId; - if (attachment && attachment != "") { - const upload = await new CloudUtils.CloudUpload({ - file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }), - isClip: false, - isThumbnail: false, - platform: 1, - }, dmChannelId, false, 0); - upload.on("complete", async () => { - const messageId = await RestAPI.post({ - url: `/channels/${dmChannelId}/messages`, - body: { - content, - attachments: [{ - id: "0", - filename: upload.filename, - uploaded_filename: upload.uploadedFilename, - }], - nonce: SnowflakeUtils.fromTimestamp(Date.now()), - }, - }).then((response) => response.body.id); - - await sleep(500); - RestAPI.delete({ - url: `/channels/${dmChannelId}/messages/${messageId}` - }); - }); - await upload.upload(); - return; - } - - const messageId = await RestAPI.post({ - url: `/channels/${dmChannelId}/messages`, - body: { - content, - nonce: SnowflakeUtils.fromTimestamp(Date.now()), - }, - }).then((response) => response.body.id); - - await sleep(500); - RestAPI.delete({ - url: `/channels/${dmChannelId}/messages/${messageId}` - }); -} - -// Handle leaving group -async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) { - const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => { - if (memberId !== senderId) { - result[memberId] = encryptcordGroupMembers[memberId]; - if (result[memberId].child == senderId) { - result[memberId].child = encryptcordGroupMembers[senderId].child; - } - if (result[memberId].parent == senderId) { - result[memberId].parent = encryptcordGroupMembers[senderId].parent; - } - } - return result; - }, {}); - - await DataStore.set('encryptcordGroupMembers', updatedMembers); - - await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2)); -} - -// Handle receiving message -async function handleMessage(message, senderId: string, groupChannel: string) { - const decryptedMessage = await decryptData(await DataStore.get("encryptcordPrivateKey"), message); - await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); -} - -// Handle receiving group data -async function handleGroupData(groupData) { - await DataStore.set('encryptcordChannelId', groupData.channel); - await DataStore.set('encryptcordGroupMembers', groupData.members); - await DataStore.set('encryptcordGroup', true); - await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7)); - setEnabled(true); -} - -// Handle joining group -async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) { - encryptcordGroupMembers[senderId] = { key: senderKey, parent: UserStore.getCurrentUser().id, child: null }; - encryptcordGroupMembers[UserStore.getCurrentUser().id].child = senderId; - await DataStore.set('encryptcordGroupMembers', encryptcordGroupMembers); - const groupChannel = await DataStore.get('encryptcordChannelId'); - const newMember = await UserUtils.getUser(senderId).catch(() => null); - if (!newMember) return; - - const membersData = {}; - Object.entries(encryptcordGroupMembers) - .forEach(([memberId, value]) => { - membersData[memberId] = value; - }); - - const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel }); - - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - await sendTempMessage(groupMember.id, membersDataString, `groupdata`); - }); - - await Promise.all(dmPromises); - await MessageActions.receiveMessage(groupChannel, { - ...await createMessage("", senderId, groupChannel, 7), components: [{ - type: 1, - components: [{ - type: 2, - style: 4, - label: 'I don\'t want to talk to you!', - custom_id: 'removeFromSelf' - }, - { - type: 2, - style: 2, - label: '(Other users can still send/receive messages to/from them)', - disabled: true, - custom_id: 'encryptcord' - }] - }] - }); -} - -// Create message for group -async function createMessage(message: string, senderId: string, channelId: string, type: number) { - const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); - const sender = await UserUtils.getUser(senderId).catch(() => null); - if (!sender) return; - return { ...messageStart, content: message, author: sender, type, flags: 0 }; -} - -// Start E2EE Group -async function startGroup(opts, ctx) { - const channelId = ctx.channel.id; - await DataStore.set('encryptcordChannelId', channelId); - await DataStore.set('encryptcordGroupMembers', { - [UserStore.getCurrentUser().id]: { key: await DataStore.get("encryptcordPublicKey"), parent: null, child: null } - }); - await DataStore.set('encryptcordGroup', true); - sendBotMessage(channelId, { content: "Group created!\n> Other users can click the lock icon to join." }); - await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7)); - setEnabled(true); -} - -// Leave the Group; -async function leave(opts, ctx) { - const channelId = ctx.channel.id; - if (!(await DataStore.get('encryptcordGroup'))) { - sendBotMessage(channelId, { content: `You're not in a group!` }); - return; - } - const user = UserStore.getCurrentUser(); - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - - const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { - const groupMember = await UserUtils.getUser(memberId).catch(() => null); - if (!groupMember) return; - await sendTempMessage(groupMember.id, "", `leaving`); - }); - - await Promise.all(dmPromises); - await DataStore.set('encryptcordGroup', false); - await DataStore.set('encryptcordChannelId', ""); - await DataStore.set('encryptcordGroupMembers', {}); - await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2)); - setEnabled(false); -} - -// View user data -async function data(opts, ctx) { - const channelId = ctx.channel.id; - const encryptcordGroupMembers = await DataStore.get('encryptcordGroupMembers'); - const encryptcordPublicKey = await DataStore.get('encryptcordPublicKey'); - const encryptcordPrivateKey = await DataStore.get('encryptcordPrivateKey'); - const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", encryptcordPrivateKey); - const groupMembers = Object.keys(encryptcordGroupMembers); - sendBotMessage(channelId, { - content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||*(DO **NOT** SHARE THIS)*\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\`` - }); -} - diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx deleted file mode 100644 index 80e3ca35fa..0000000000 --- a/src/plugins/encryptcord/rsa-utils.tsx +++ /dev/null @@ -1,116 +0,0 @@ -export const generateKeys = async () => { - const keyPair = await crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["encrypt", "decrypt"] - ); - - const exportedPublicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); - const publicKey = formatPemKey(exportedPublicKey, "public"); - - return { privateKey: keyPair.privateKey, publicKey }; -}; - -export const encryptData = async (pemPublicKey, data) => { - const publicKey = await importPemPublicKey(pemPublicKey); - - const chunkSize = 446; - - const encryptedChunks: any[] = []; - const encoder = new TextEncoder(); - - for (let i = 0; i < data.length; i += chunkSize) { - const chunk = await data.substring(i, i + chunkSize); - const encryptedChunk = await crypto.subtle.encrypt( - { - name: "RSA-OAEP", - }, - publicKey, - encoder.encode(chunk) - ); - encryptedChunks.push(arrayBufferToBase64(encryptedChunk)); - } - - return encryptedChunks; -}; - -export const decryptData = async (privateKey, encArray) => { - const decryptionPromises = encArray.map(async (encStr) => { - const encBuffer = base64ToArrayBuffer(encStr); - - const dec = await crypto.subtle.decrypt( - { - name: "RSA-OAEP", - }, - privateKey, - encBuffer - ); - - return new TextDecoder().decode(dec); - }); - - const decryptedMessages = await Promise.all(decryptionPromises); - - return decryptedMessages.join(''); -}; - -// Helper functions -const arrayBufferToBase64 = (buffer) => { - const binary = String.fromCharCode(...new Uint8Array(buffer)); - return btoa(binary); -}; - -const base64ToArrayBuffer = (base64String) => { - const binaryString = atob(base64String); - const length = binaryString.length; - const buffer = new ArrayBuffer(length); - const view = new Uint8Array(buffer); - - for (let i = 0; i < length; i++) { - view[i] = binaryString.charCodeAt(i); - } - - return buffer; -}; - -export const formatPemKey = (keyData, type) => { - const base64Key = arrayBufferToBase64(keyData); - return `-----BEGIN ${type.toUpperCase()} KEY-----\n` + base64Key + `\n-----END ${type.toUpperCase()} KEY----- `; -}; - -const importPemPublicKey = async (pemKey) => { - try { - const trimmedPemKey = pemKey.trim(); - - const keyBody = trimmedPemKey - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", ""); - - const binaryDer = atob(keyBody); - - const arrayBuffer = new Uint8Array(binaryDer.length); - for (let i = 0; i < binaryDer.length; i++) { - arrayBuffer[i] = binaryDer.charCodeAt(i); - } - - return await crypto.subtle.importKey( - "spki", - arrayBuffer, - { - name: "RSA-OAEP", - hash: { name: "SHA-256" }, - }, - true, - ["encrypt"] - ); - } catch (error) { - console.error("Error importing PEM public key:", error); - throw error; - } -}; - From 4fa67ca35ed3d016c96fd68b6dc2a5391ae227ff Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 02:25:27 -0800 Subject: [PATCH 22/58] Fixed notification --- src/plugins/bypassdnd/index.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx index 1a826a6e17..4d9096eb5c 100644 --- a/src/plugins/bypassdnd/index.tsx +++ b/src/plugins/bypassdnd/index.tsx @@ -1,7 +1,6 @@ import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; -import { showNotification } from "@api/Notifications"; import { definePluginSettings } from "@api/Settings"; -import { DataStore } from "@api/index"; +import { DataStore, Notifications } from "@api/index"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, PrivateChannelsStore, UserStore } from "@webpack/common"; @@ -123,18 +122,18 @@ export default definePlugin({ const { guilds, channels, users } = bypasses; if ((guilds.includes(guildId) || channels.includes(channelId)) && (message.content.includes(`<@${UserStore.getCurrentUser().id}>`) || message.mentions.some(mention => mention.id === UserStore.getCurrentUser().id))) { - await showNotification({ + await Notifications.showNotification({ title: `${message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, body: message.content, - onClick: () => window.location.href = `https://discord.com/channels/${guildId}/${channelId}/${message.id}` + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), }); return; } if (users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await showNotification({ + await Notifications.showNotification({ title: `${message.author.username} sent a message in a DM`, body: message.content, - onClick: () => window.location.href = `https://discord.com/channels/@me/${channelId}/${message.id}` + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), }); } } From 11f05ed1fbcd7cbb2c852976fa33ee9b17335ffb Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 07:28:07 -0800 Subject: [PATCH 23/58] Added filtering so it only works in dnd --- src/plugins/bypassdnd/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx index 4d9096eb5c..d9f21d098e 100644 --- a/src/plugins/bypassdnd/index.tsx +++ b/src/plugins/bypassdnd/index.tsx @@ -3,7 +3,7 @@ import { definePluginSettings } from "@api/Settings"; import { DataStore, Notifications } from "@api/index"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; import { Channel, Guild, Message, User } from "discord-types/general"; interface ContextProps { channel: Channel; @@ -119,19 +119,19 @@ export default definePlugin({ if (message.state === "SENDING") return; if (message.author.id === UserStore.getCurrentUser().id) return; if (!message.content) return; + if (await PresenceStore.getStatus(UserStore.getCurrentUser().id) != 'dnd') return; - const { guilds, channels, users } = bypasses; - if ((guilds.includes(guildId) || channels.includes(channelId)) && (message.content.includes(`<@${UserStore.getCurrentUser().id}>`) || message.mentions.some(mention => mention.id === UserStore.getCurrentUser().id))) { + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${UserStore.getCurrentUser().id}>`) || message.mentions.some(mention => mention.id === UserStore.getCurrentUser().id))) { await Notifications.showNotification({ - title: `${message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, + title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), }); return; } - if (users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await Notifications.showNotification({ - title: `${message.author.username} sent a message in a DM`, + title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), }); From ba8a5aa537a72dfedba78304c6a6a325756b082d Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 07:30:29 -0800 Subject: [PATCH 24/58] Lengthened description --- src/plugins/bypassdnd/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx index d9f21d098e..d5e14ecc2f 100644 --- a/src/plugins/bypassdnd/index.tsx +++ b/src/plugins/bypassdnd/index.tsx @@ -111,7 +111,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "BypassDND", - description: "Still get notifications from specific sources.", + description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", authors: [Devs.Inbestigator], flux: { async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { From 0f042bc3422ae1d0185336968ab5b7512dbf535f Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 07:51:53 -0800 Subject: [PATCH 25/58] Cleaned up the flux function a bit --- src/plugins/bypassdnd/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx index d5e14ecc2f..c076ad1359 100644 --- a/src/plugins/bypassdnd/index.tsx +++ b/src/plugins/bypassdnd/index.tsx @@ -117,11 +117,11 @@ export default definePlugin({ async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { if (optimistic || type !== "MESSAGE_CREATE") return; if (message.state === "SENDING") return; - if (message.author.id === UserStore.getCurrentUser().id) return; if (!message.content) return; - if (await PresenceStore.getStatus(UserStore.getCurrentUser().id) != 'dnd') return; - - if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${UserStore.getCurrentUser().id}>`) || message.mentions.some(mention => mention.id === UserStore.getCurrentUser().id))) { + const currentUser = UserStore.getCurrentUser(); + if (message.author.id === currentUser.id) return; + if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { await Notifications.showNotification({ title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, body: message.content, From 245b274a2478a77d367272aef2dc8012ddda98a4 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 08:02:19 -0800 Subject: [PATCH 26/58] Renaming folder --- src/plugins/bypassDND/s | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/plugins/bypassDND/s diff --git a/src/plugins/bypassDND/s b/src/plugins/bypassDND/s new file mode 100644 index 0000000000..e69de29bb2 From bc2eff8e9f44597395584b6561cecd8299cbf1fb Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:03:23 -0800 Subject: [PATCH 27/58] Update and rename s to index.tsx --- src/plugins/bypassDND/index.tsx | 154 ++++++++++++++++++++++++++++++++ src/plugins/bypassDND/s | 0 2 files changed, 154 insertions(+) create mode 100644 src/plugins/bypassDND/index.tsx delete mode 100644 src/plugins/bypassDND/s diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx new file mode 100644 index 0000000000..93e7500e1c --- /dev/null +++ b/src/plugins/bypassDND/index.tsx @@ -0,0 +1,154 @@ +import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { DataStore, Notifications } from "@api/index"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { Channel, Guild, Message, User } from "discord-types/general"; +interface ContextProps { + channel: Channel; + user: User; + guild: Guild; +} + +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + guildId: string; + message: Message; +} + +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { + if (!guild) return; + children.splice(-1, 0, ( + + { + if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); + else bypasses["guilds"].push(guild.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.guilds = (bypasses["guilds"].join(', ')); + }} + /> + + )); +}; + +const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { + if (!channel) return; + children.splice(-1, 0, ( + + { + if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); + else bypasses["channels"].push(channel.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.channels = (bypasses["channels"].join(', ')); + }} + /> + + )); +}; + +const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { + if (!user) return; + children.splice(-1, 0, ( + + { + if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); + else bypasses["users"].push(user.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.users = (bypasses["users"].join(', ')); + }} + /> + + )); +}; + +let bypasses; + +const settings = definePluginSettings({ + guilds: { + type: OptionType.STRING, + description: "Guilds to let bypass (notified when pinged anywhere in guild)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["guild"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + channels: { + type: OptionType.STRING, + description: "Channels to let bypass (notified when pinged in that channel)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + users: { + type: OptionType.STRING, + description: "Users to let bypass (notified for all messages)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + } +}); + +export default definePlugin({ + name: "BypassDND", + description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", + authors: [Devs.Inbestigator], + flux: { + async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content) return; + const currentUser = UserStore.getCurrentUser(); + if (message.author.id === currentUser.id) return; + if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); + return; + } + if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); + } + } + }, + settings, + async start() { + addContextMenuPatch("guild-context", GuildContext); + addContextMenuPatch("channel-context", ChannelContext); + addContextMenuPatch("user-context", UserContext); + bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; + await DataStore.set("bypassdnd", bypasses); + }, + stop() { + removeContextMenuPatch("guild-context", GuildContext); + removeContextMenuPatch("channel-context", ChannelContext); + removeContextMenuPatch("user-context", UserContext); + } +}); diff --git a/src/plugins/bypassDND/s b/src/plugins/bypassDND/s deleted file mode 100644 index e69de29bb2..0000000000 From 76b5b1e8699c0120c39c41fa3c7a75b99792d051 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:03:45 -0800 Subject: [PATCH 28/58] Delete original bypassdnd directory --- src/plugins/bypassdnd/index.tsx | 154 -------------------------------- 1 file changed, 154 deletions(-) delete mode 100644 src/plugins/bypassdnd/index.tsx diff --git a/src/plugins/bypassdnd/index.tsx b/src/plugins/bypassdnd/index.tsx deleted file mode 100644 index c076ad1359..0000000000 --- a/src/plugins/bypassdnd/index.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { DataStore, Notifications } from "@api/index"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; -import { Channel, Guild, Message, User } from "discord-types/general"; -interface ContextProps { - channel: Channel; - user: User; - guild: Guild; -} - -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - guildId: string; - message: Message; -} - -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { - if (!guild) return; - children.splice(-1, 0, ( - - { - if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); - else bypasses["guilds"].push(guild.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.guilds = (bypasses["guilds"].join(', ')); - }} - /> - - )); -}; - -const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { - if (!channel) return; - children.splice(-1, 0, ( - - { - if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); - else bypasses["channels"].push(channel.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.channels = (bypasses["channels"].join(', ')); - }} - /> - - )); -}; - -const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { - if (!user) return; - children.splice(-1, 0, ( - - { - if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); - else bypasses["users"].push(user.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.users = (bypasses["users"].join(', ')); - }} - /> - - )); -}; - -let bypasses; - -const settings = definePluginSettings({ - guilds: { - type: OptionType.STRING, - description: "Guilds to let bypass (notified when pinged anywhere in guild)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["guild"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - }, - channels: { - type: OptionType.STRING, - description: "Channels to let bypass (notified when pinged in that channel)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - }, - users: { - type: OptionType.STRING, - description: "Users to let bypass (notified for all messages)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - } -}); - -export default definePlugin({ - name: "BypassDND", - description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", - authors: [Devs.Inbestigator], - flux: { - async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (!message.content) return; - const currentUser = UserStore.getCurrentUser(); - if (message.author.id === currentUser.id) return; - if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; - if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); - return; - } - if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); - } - } - }, - settings, - async start() { - addContextMenuPatch("guild-context", GuildContext); - addContextMenuPatch("channel-context", ChannelContext); - addContextMenuPatch("user-context", UserContext); - bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; - await DataStore.set("bypassdnd", bypasses); - }, - stop() { - removeContextMenuPatch("guild-context", GuildContext); - removeContextMenuPatch("channel-context", ChannelContext); - removeContextMenuPatch("user-context", UserContext); - } -}); From ee9e44d2d736a6a97fb1bc5f92c0c2e41cb8a4fe Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Wed, 6 Mar 2024 08:38:52 -0800 Subject: [PATCH 29/58] Update pnpm-lock.yaml --- pnpm-lock.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d7ec54182..43866f50ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - patchedDependencies: eslint-plugin-path-alias@1.0.0: hash: m6sma4g6bh67km3q6igf6uxaja @@ -617,8 +613,8 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true - /@types/yauzl@2.10.3: - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + /@types/yauzl@2.10.0: + resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: '@types/node': 18.16.3 @@ -1703,7 +1699,7 @@ packages: get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: - '@types/yauzl': 2.10.3 + '@types/yauzl': 2.10.0 transitivePeerDependencies: - supports-color dev: true @@ -1814,8 +1810,8 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true @@ -3231,7 +3227,7 @@ packages: '@esbuild-kit/core-utils': 3.1.0 '@esbuild-kit/esm-loader': 2.5.5 optionalDependencies: - fsevents: 2.3.3 + fsevents: 2.3.2 dev: true /type-check@0.4.0: @@ -3468,3 +3464,7 @@ packages: name: gifenc version: 1.0.3 dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From c090b8b3298e9effe0a8a19c59a29db924d56d8b Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 08:41:17 -0800 Subject: [PATCH 30/58] Added types to bypasses object --- src/plugins/bypassDND/index.tsx | 318 ++++++++++++++++---------------- 1 file changed, 164 insertions(+), 154 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 93e7500e1c..ba567b58f5 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -1,154 +1,164 @@ -import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { DataStore, Notifications } from "@api/index"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; -import { Channel, Guild, Message, User } from "discord-types/general"; -interface ContextProps { - channel: Channel; - user: User; - guild: Guild; -} - -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - guildId: string; - message: Message; -} - -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { - if (!guild) return; - children.splice(-1, 0, ( - - { - if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); - else bypasses["guilds"].push(guild.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.guilds = (bypasses["guilds"].join(', ')); - }} - /> - - )); -}; - -const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { - if (!channel) return; - children.splice(-1, 0, ( - - { - if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); - else bypasses["channels"].push(channel.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.channels = (bypasses["channels"].join(', ')); - }} - /> - - )); -}; - -const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { - if (!user) return; - children.splice(-1, 0, ( - - { - if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); - else bypasses["users"].push(user.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.users = (bypasses["users"].join(', ')); - }} - /> - - )); -}; - -let bypasses; - -const settings = definePluginSettings({ - guilds: { - type: OptionType.STRING, - description: "Guilds to let bypass (notified when pinged anywhere in guild)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["guild"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - }, - channels: { - type: OptionType.STRING, - description: "Channels to let bypass (notified when pinged in that channel)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - }, - users: { - type: OptionType.STRING, - description: "Users to let bypass (notified for all messages)", - default: "", - placeholder: "Separate with commas", - onChange: async function (value) { - bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); - await DataStore.set("bypassdnd", bypasses); - }, - } -}); - -export default definePlugin({ - name: "BypassDND", - description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", - authors: [Devs.Inbestigator], - flux: { - async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (!message.content) return; - const currentUser = UserStore.getCurrentUser(); - if (message.author.id === currentUser.id) return; - if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; - if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); - return; - } - if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); - } - } - }, - settings, - async start() { - addContextMenuPatch("guild-context", GuildContext); - addContextMenuPatch("channel-context", ChannelContext); - addContextMenuPatch("user-context", UserContext); - bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; - await DataStore.set("bypassdnd", bypasses); - }, - stop() { - removeContextMenuPatch("guild-context", GuildContext); - removeContextMenuPatch("channel-context", ChannelContext); - removeContextMenuPatch("user-context", UserContext); - } -}); +import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { DataStore, Notifications } from "@api/index"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { Channel, Guild, Message, User } from "discord-types/general"; +interface ContextProps { + channel: Channel; + user: User; + guild: Guild; +} + +interface IMessageCreate { + type: "MESSAGE_CREATE"; + optimistic: boolean; + isPushNotification: boolean; + channelId: string; + guildId: string; + message: Message; +} + +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { + if (!guild) return; + children.splice(-1, 0, ( + + { + if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); + else bypasses["guilds"].push(guild.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.guilds = (bypasses["guilds"].join(', ')); + }} + /> + + )); +}; + +const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { + if (!channel) return; + children.splice(-1, 0, ( + + { + if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); + else bypasses["channels"].push(channel.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.channels = (bypasses["channels"].join(', ')); + }} + /> + + )); +}; + +const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { + if (!user) return; + children.splice(-1, 0, ( + + { + if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); + else bypasses["users"].push(user.id); + await DataStore.set("bypassdnd", bypasses); + settings.store.users = (bypasses["users"].join(', ')); + }} + /> + + )); +}; + +type Bypasses = { + guilds: string[]; + channels: string[]; + users: string[]; +}; + +let bypasses: Bypasses = { + guilds: [], + channels: [], + users: [], +}; + +const settings = definePluginSettings({ + guilds: { + type: OptionType.STRING, + description: "Guilds to let bypass (notified when pinged anywhere in guild)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["guilds"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + channels: { + type: OptionType.STRING, + description: "Channels to let bypass (notified when pinged in that channel)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + }, + users: { + type: OptionType.STRING, + description: "Users to let bypass (notified for all messages)", + default: "", + placeholder: "Separate with commas", + onChange: async function (value) { + bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + await DataStore.set("bypassdnd", bypasses); + }, + } +}); + +export default definePlugin({ + name: "BypassDND", + description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", + authors: [Devs.Inbestigator], + flux: { + async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content) return; + const currentUser = UserStore.getCurrentUser(); + if (message.author.id === currentUser.id) return; + if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); + return; + } + if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); + } + } + }, + settings, + async start() { + addContextMenuPatch("guild-context", GuildContext); + addContextMenuPatch("channel-context", ChannelContext); + addContextMenuPatch("user-context", UserContext); + bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; + await DataStore.set("bypassdnd", bypasses); + }, + stop() { + removeContextMenuPatch("guild-context", GuildContext); + removeContextMenuPatch("channel-context", ChannelContext); + removeContextMenuPatch("user-context", UserContext); + } +}); From 58d64f7e77d60b8656ad85f737c8f0a3fb460460 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 08:44:57 -0800 Subject: [PATCH 31/58] Removed unnecessary bit --- src/plugins/bypassDND/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index ba567b58f5..6b324f6924 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -80,11 +80,7 @@ type Bypasses = { users: string[]; }; -let bypasses: Bypasses = { - guilds: [], - channels: [], - users: [], -}; +let bypasses: Bypasses; const settings = definePluginSettings({ guilds: { From daee463e2a34ca9c43e08eaf196111cc2eca0205 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 08:54:05 -0800 Subject: [PATCH 32/58] Let bypass users create notification outside of DM --- src/plugins/bypassDND/index.tsx | 34 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 6b324f6924..cdc4b37b05 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -115,6 +115,22 @@ const settings = definePluginSettings({ } }); +async function showUserNotification(message: Message) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); +} + +async function showChannelNotification(message: Message) { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(message.channel_id).name}`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + }); +} + export default definePlugin({ name: "BypassDND", description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", @@ -128,19 +144,15 @@ export default definePlugin({ if (message.author.id === currentUser.id) return; if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(channelId).name}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); + await showChannelNotification(message); return; } - if (bypasses.users.includes(message.author.id) && channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - }); + if (bypasses.users.includes(message.author.id)) { + if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + await showUserNotification(message); + } else if (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) { + await showChannelNotification(message); + } } } }, From 21654c8fd9de4e5514701976b638d71e7bf7b1af Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 09:01:46 -0800 Subject: [PATCH 33/58] Made the allow in all channels option a toggle --- src/plugins/bypassDND/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index cdc4b37b05..cc22feeb73 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -112,6 +112,10 @@ const settings = definePluginSettings({ bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); await DataStore.set("bypassdnd", bypasses); }, + }, + allowOutsideOfDM: { + type: OptionType.BOOLEAN, + description: "Allow selected users to send notifications outside of DM (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", } }); @@ -150,7 +154,7 @@ export default definePlugin({ if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showUserNotification(message); - } else if (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) { + } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDM) { await showChannelNotification(message); } } From 9bb33f78c64a00b3c80482ddb4f2165f99a6d635 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 09:09:43 -0800 Subject: [PATCH 34/58] Fixed spelling mistake --- src/plugins/bypassDND/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index cc22feeb73..92520cdc32 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -113,7 +113,7 @@ const settings = definePluginSettings({ await DataStore.set("bypassdnd", bypasses); }, }, - allowOutsideOfDM: { + allowOutsideOfDm: { type: OptionType.BOOLEAN, description: "Allow selected users to send notifications outside of DM (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", } @@ -154,7 +154,7 @@ export default definePlugin({ if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showUserNotification(message); - } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDM) { + } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDm) { await showChannelNotification(message); } } From 4f218da3192d5981d5cd39b1a735fb4bff9addcf Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 09:11:22 -0800 Subject: [PATCH 35/58] Added clarification --- src/plugins/bypassDND/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 92520cdc32..2ccd0cc5cd 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -105,7 +105,7 @@ const settings = definePluginSettings({ }, users: { type: OptionType.STRING, - description: "Users to let bypass (notified for all messages)", + description: "Users to let bypass (notified for all messages sent in DMs)", default: "", placeholder: "Separate with commas", onChange: async function (value) { @@ -115,7 +115,7 @@ const settings = definePluginSettings({ }, allowOutsideOfDm: { type: OptionType.BOOLEAN, - description: "Allow selected users to send notifications outside of DM (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", + description: "Allow selected users to bypass DND outside of DM too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", } }); From 0e6f19ff6724b192d66c7e7b20ef8a997069c361 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 09:12:55 -0800 Subject: [PATCH 36/58] =?UTF-8?q?Consistency=20=F0=9F=91=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/bypassDND/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 2ccd0cc5cd..dd98acc2d4 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -113,9 +113,9 @@ const settings = definePluginSettings({ await DataStore.set("bypassdnd", bypasses); }, }, - allowOutsideOfDm: { + allowOutsideOfDms: { type: OptionType.BOOLEAN, - description: "Allow selected users to bypass DND outside of DM too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", + description: "Allow selected users to bypass DND outside of DMs too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", } }); @@ -154,7 +154,7 @@ export default definePlugin({ if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showUserNotification(message); - } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDm) { + } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDms) { await showChannelNotification(message); } } From 733de54d016c756664411dd15eec6a03991ad95a Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:10:56 -0800 Subject: [PATCH 37/58] Trycatch --- src/plugins/bypassDND/index.tsx | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index dd98acc2d4..111e04b4f5 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -141,22 +141,26 @@ export default definePlugin({ authors: [Devs.Inbestigator], flux: { async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (!message.content) return; - const currentUser = UserStore.getCurrentUser(); - if (message.author.id === currentUser.id) return; - if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; - if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { - await showChannelNotification(message); - return; - } - if (bypasses.users.includes(message.author.id)) { - if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await showUserNotification(message); - } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDms) { + try { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content) return; + const currentUser = UserStore.getCurrentUser(); + if (message.author.id === currentUser.id) return; + if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { await showChannelNotification(message); + return; + } + if (bypasses.users.includes(message.author.id)) { + if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { + await showUserNotification(message); + } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDms) { + await showChannelNotification(message); + } } + } catch (error) { + console.error(error); } } }, From 68412b98796fe669ce27c2044e1cffb29df9cf16 Mon Sep 17 00:00:00 2001 From: Inbestigator <119569726+Inbestigator@users.noreply.github.com> Date: Wed, 6 Mar 2024 19:29:50 +0000 Subject: [PATCH 38/58] Fixed all Lint issues, formatted script --- src/plugins/bypassDND/index.tsx | 107 +++++++++++++++++++------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 111e04b4f5..75a0b1b609 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -1,10 +1,17 @@ -import { NavContextMenuPatchCallback, addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { addContextMenuPatch, type NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { DataStore, Notifications } from "@api/index"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; -import { Channel, Guild, Message, User } from "discord-types/general"; +import { type Channel, type Guild, type Message, type User } from "discord-types/general"; + interface ContextProps { channel: Channel; user: User; @@ -21,64 +28,80 @@ interface IMessageCreate { } const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { - if (!guild) return; children.splice(-1, 0, ( { - if (bypasses["guilds"].includes(guild.id)) bypasses["guilds"] = await bypasses["guilds"].filter(id => id !== guild.id); - else bypasses["guilds"].push(guild.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.guilds = (bypasses["guilds"].join(', ')); + label={`${bypasses.guilds.includes(guild.id) ? "Remove" : "Add"} DND Bypass`} + action={() => { + if (bypasses.guilds.includes(guild.id)) bypasses.guilds = bypasses.guilds.filter(id => id !== guild.id); + else bypasses.guilds.push(guild.id); + DataStore.set("bypassdnd", bypasses) + .then(() => { + settings.store.guilds = bypasses.guilds.join(", "); + }) + .catch(error => { + console.error(error); + }); }} + /> )); }; const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { - if (!channel) return; children.splice(-1, 0, ( { - if (bypasses["channels"].includes(channel.id)) bypasses["channels"] = await bypasses["channels"].filter(id => id !== channel.id); - else bypasses["channels"].push(channel.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.channels = (bypasses["channels"].join(', ')); + label={`${bypasses.channels.includes(channel.id) ? "Remove" : "Add"} DND Bypass`} + action={() => { + if (bypasses.channels.includes(channel.id)) bypasses.channels = bypasses.channels.filter(id => id !== channel.id); + else bypasses.channels.push(channel.id); + + DataStore.set("bypassdnd", bypasses) + .then(() => { + settings.store.channels = bypasses.channels.join(", "); + }) + .catch(error => { + console.error(error); + }); }} + /> )); }; const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { - if (!user) return; children.splice(-1, 0, ( { - if (bypasses["users"].includes(user.id)) bypasses["users"] = await bypasses["users"].filter(id => id !== user.id); - else bypasses["users"].push(user.id); - await DataStore.set("bypassdnd", bypasses); - settings.store.users = (bypasses["users"].join(', ')); + label={`${bypasses.users.includes(user.id) ? "Remove" : "Add"} DND Bypass`} + action={() => { + if (bypasses.users.includes(user.id)) bypasses.users = bypasses.users.filter(id => id !== user.id); + else bypasses.users.push(user.id); + + DataStore.set("bypassdnd", bypasses) + .then(() => { + settings.store.users = bypasses.users.join(", "); + }) + .catch(error => { + console.error(error); + }); }} /> )); }; -type Bypasses = { +interface Bypasses { guilds: string[]; channels: string[]; users: string[]; -}; +} let bypasses: Bypasses; @@ -89,9 +112,9 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses["guilds"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + bypasses.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); await DataStore.set("bypassdnd", bypasses); - }, + } }, channels: { type: OptionType.STRING, @@ -99,9 +122,9 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses["channels"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + bypasses.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); await DataStore.set("bypassdnd", bypasses); - }, + } }, users: { type: OptionType.STRING, @@ -109,29 +132,29 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses["users"] = value.replace(/\s/g, '').split(',').filter(id => id.trim() !== ''); + bypasses.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); await DataStore.set("bypassdnd", bypasses); - }, + } }, allowOutsideOfDms: { type: OptionType.BOOLEAN, - description: "Allow selected users to bypass DND outside of DMs too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)", + description: "Allow selected users to bypass DND outside of DMs too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)" } }); -async function showUserNotification(message: Message) { +async function showUserNotification(message: Message): Promise { await Notifications.showNotification({ title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false) }); } -async function showChannelNotification(message: Message) { +async function showChannelNotification(message: Message): Promise { await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(message.channel_id).name}`, + title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}`, body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false) }); } @@ -144,10 +167,10 @@ export default definePlugin({ try { if (optimistic || type !== "MESSAGE_CREATE") return; if (message.state === "SENDING") return; - if (!message.content) return; + if (message.content === "") return; const currentUser = UserStore.getCurrentUser(); if (message.author.id === currentUser.id) return; - if (await PresenceStore.getStatus(currentUser.id) != 'dnd') return; + if (await PresenceStore.getStatus(currentUser.id) !== "dnd") return; if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { await showChannelNotification(message); return; @@ -155,7 +178,7 @@ export default definePlugin({ if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showUserNotification(message); - } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && settings.store.allowOutsideOfDms) { + } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && (settings.store.allowOutsideOfDms === true)) { await showChannelNotification(message); } } @@ -169,7 +192,7 @@ export default definePlugin({ addContextMenuPatch("guild-context", GuildContext); addContextMenuPatch("channel-context", ChannelContext); addContextMenuPatch("user-context", UserContext); - bypasses = await DataStore.get("bypassdnd") ?? { guilds: [], channels: [], users: [] }; + bypasses = (await DataStore.get("bypassdnd")) ?? { guilds: [], channels: [], users: [] }; await DataStore.set("bypassdnd", bypasses); }, stop() { From 22d692112de75f2e09832c61a84f8beceb2e7f9b Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 6 Mar 2024 16:10:26 -0800 Subject: [PATCH 39/58] Combined notification functions --- src/plugins/bypassDND/index.tsx | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 75a0b1b609..7ff671cef6 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -9,7 +9,7 @@ import { DataStore, Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { ChannelStore, Menu, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; import { type Channel, type Guild, type Message, type User } from "discord-types/general"; interface ContextProps { @@ -142,19 +142,14 @@ const settings = definePluginSettings({ } }); -async function showUserNotification(message: Message): Promise { +async function showNotification(message: Message, guildId?: string): Promise { await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in a DM`, + title: `${message.author.globalName ?? message.author.username} ${guildId ? `sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}` : "sent a message in a DM"}`, body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false) - }); -} - -async function showChannelNotification(message: Message): Promise { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false) + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + onClick: function () { + NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); + } }); } @@ -172,14 +167,14 @@ export default definePlugin({ if (message.author.id === currentUser.id) return; if (await PresenceStore.getStatus(currentUser.id) !== "dnd") return; if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && (message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id))) { - await showChannelNotification(message); + await showNotification(message, guildId); return; } if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await showUserNotification(message); + await showNotification(message); } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && (settings.store.allowOutsideOfDms === true)) { - await showChannelNotification(message); + await showNotification(message, guildId); } } } catch (error) { From 90c460e9aed83926688cc2746855d63151e4c2c6 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 7 Mar 2024 16:44:01 -0800 Subject: [PATCH 40/58] Added context icon --- src/plugins/bypassDND/index.tsx | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 7ff671cef6..620aebde4d 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -27,14 +27,26 @@ interface IMessageCreate { message: Message; } +function icon(enabled?: boolean) { + return + + + ; +} + const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { + const enabled = bypasses.guilds.includes(guild.id); children.splice(-1, 0, ( icon(enabled)} action={() => { - if (bypasses.guilds.includes(guild.id)) bypasses.guilds = bypasses.guilds.filter(id => id !== guild.id); + if (enabled) bypasses.guilds = bypasses.guilds.filter(id => id !== guild.id); else bypasses.guilds.push(guild.id); DataStore.set("bypassdnd", bypasses) .then(() => { @@ -51,13 +63,15 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextP }; const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { + const enabled = bypasses.channels.includes(channel.id); children.splice(-1, 0, ( icon(enabled)} action={() => { - if (bypasses.channels.includes(channel.id)) bypasses.channels = bypasses.channels.filter(id => id !== channel.id); + if (enabled) bypasses.channels = bypasses.channels.filter(id => id !== channel.id); else bypasses.channels.push(channel.id); DataStore.set("bypassdnd", bypasses) @@ -75,13 +89,15 @@ const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: Cont }; const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { + const enabled = bypasses.users.includes(user.id); children.splice(-1, 0, ( icon(enabled)} action={() => { - if (bypasses.users.includes(user.id)) bypasses.users = bypasses.users.filter(id => id !== user.id); + if (enabled) bypasses.users = bypasses.users.filter(id => id !== user.id); else bypasses.users.push(user.id); DataStore.set("bypassdnd", bypasses) From 68c1f7a8db5b6634d9d7889a683c310f32fe5c56 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 7 Mar 2024 17:25:36 -0800 Subject: [PATCH 41/58] Fixed mentioned checks --- src/plugins/bypassDND/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 620aebde4d..72a5dcf7da 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -9,7 +9,7 @@ import { DataStore, Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; import { type Channel, type Guild, type Message, type User } from "discord-types/general"; interface ContextProps { @@ -90,6 +90,7 @@ const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: Cont const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { const enabled = bypasses.users.includes(user.id); + if (user.id === UserStore.getCurrentUser().id) return; children.splice(-1, 0, ( `) || message.mentions.some(mention => mention.id === currentUser.id))) { + const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; + if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && mentioned) { await showNotification(message, guildId); return; } if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showNotification(message); - } else if ((message.content.includes(`<@${currentUser.id}>`) || message.mentions.some(mention => mention.id === currentUser.id)) && (settings.store.allowOutsideOfDms === true)) { + } else if (mentioned && (settings.store.allowOutsideOfDms === true)) { await showNotification(message, guildId); } } From 96b6ac67ae27e30cfcff9f2017b2e2f958e3155c Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 15:56:41 -0800 Subject: [PATCH 42/58] Updated contextmenus --- src/plugins/bypassDND/index.tsx | 120 ++++++++++---------------------- 1 file changed, 35 insertions(+), 85 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 72a5dcf7da..351ba03829 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addContextMenuPatch, type NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { type NavContextMenuPatchCallback } from "@api/ContextMenu"; import { DataStore, Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; @@ -37,82 +37,35 @@ function icon(enabled?: boolean) { ; } -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: ContextProps) => () => { - const enabled = bypasses.guilds.includes(guild.id); - children.splice(-1, 0, ( - - icon(enabled)} - action={() => { - if (enabled) bypasses.guilds = bypasses.guilds.filter(id => id !== guild.id); - else bypasses.guilds.push(guild.id); - DataStore.set("bypassdnd", bypasses) - .then(() => { - settings.store.guilds = bypasses.guilds.join(", "); - }) - .catch(error => { - console.error(error); - }); - }} - - /> - - )); -}; - -const ChannelContext: NavContextMenuPatchCallback = (children, { channel }: ContextProps) => () => { - const enabled = bypasses.channels.includes(channel.id); - children.splice(-1, 0, ( - - icon(enabled)} - action={() => { - if (enabled) bypasses.channels = bypasses.channels.filter(id => id !== channel.id); - else bypasses.channels.push(channel.id); - - DataStore.set("bypassdnd", bypasses) - .then(() => { - settings.store.channels = bypasses.channels.join(", "); - }) - .catch(error => { - console.error(error); - }); - }} - - /> - - )); -}; - -const UserContext: NavContextMenuPatchCallback = (children, { user }: ContextProps) => () => { - const enabled = bypasses.users.includes(user.id); - if (user.id === UserStore.getCurrentUser().id) return; - children.splice(-1, 0, ( - - icon(enabled)} - action={() => { - if (enabled) bypasses.users = bypasses.users.filter(id => id !== user.id); - else bypasses.users.push(user.id); - - DataStore.set("bypassdnd", bypasses) - .then(() => { - settings.store.users = bypasses.users.join(", "); - }) - .catch(error => { - console.error(error); - }); - }} - /> - - )); -}; +function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatchCallback { + return (children, props) => { + const type = props[name]; + if (!type) return; + const enabled = bypasses[`${name}s`].includes(type.id); + if (name === "user" && type.id === UserStore.getCurrentUser().id) return; + children.splice(-1, 0, ( + + icon(enabled)} + action={() => { + if (enabled) bypasses[`${name}s`] = bypasses[`${name}s`].filter(id => id !== type.id); + else bypasses[`${name}s`].push(type.id); + + DataStore.set("bypassdnd", bypasses) + .then(() => { + settings.store[`${name}s`] = bypasses[`${name}s`].join(", "); + }) + .catch(error => { + console.error(error); + }); + }} + /> + + )); + }; +} interface Bypasses { guilds: string[]; @@ -201,16 +154,13 @@ export default definePlugin({ } }, settings, + contextMenus: { + "guild-context": ContextCallback("guild"), + "channel-context": ContextCallback("channel"), + "user-context": ContextCallback("user"), + }, async start() { - addContextMenuPatch("guild-context", GuildContext); - addContextMenuPatch("channel-context", ChannelContext); - addContextMenuPatch("user-context", UserContext); bypasses = (await DataStore.get("bypassdnd")) ?? { guilds: [], channels: [], users: [] }; await DataStore.set("bypassdnd", bypasses); - }, - stop() { - removeContextMenuPatch("guild-context", GuildContext); - removeContextMenuPatch("channel-context", ChannelContext); - removeContextMenuPatch("user-context", UserContext); } }); From 98ee690bdd5021f5108690ce9e71f61e4756422e Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 16:06:18 -0800 Subject: [PATCH 43/58] Removed unused interface --- src/plugins/bypassDND/index.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 351ba03829..df0af0cdee 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -10,13 +10,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; -import { type Channel, type Guild, type Message, type User } from "discord-types/general"; - -interface ContextProps { - channel: Channel; - user: User; - guild: Guild; -} +import { type Message } from "discord-types/general"; interface IMessageCreate { type: "MESSAGE_CREATE"; @@ -139,9 +133,7 @@ export default definePlugin({ const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && mentioned) { await showNotification(message, guildId); - return; - } - if (bypasses.users.includes(message.author.id)) { + } else if (bypasses.users.includes(message.author.id)) { if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { await showNotification(message); } else if (mentioned && (settings.store.allowOutsideOfDms === true)) { From ce99871a4dac450a41f4e5f1d29127282bd70d6b Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 16:12:33 -0800 Subject: [PATCH 44/58] Removed excessive number of ifs --- src/plugins/bypassDND/index.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index df0af0cdee..9f7718eb62 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -13,9 +13,7 @@ import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, Priv import { type Message } from "discord-types/general"; interface IMessageCreate { - type: "MESSAGE_CREATE"; optimistic: boolean; - isPushNotification: boolean; channelId: string; guildId: string; message: Message; @@ -122,21 +120,19 @@ export default definePlugin({ description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", authors: [Devs.Inbestigator], flux: { - async MESSAGE_CREATE({ optimistic, type, message, guildId, channelId }: IMessageCreate) { + async MESSAGE_CREATE({ optimistic, message, guildId, channelId }: IMessageCreate) { try { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (message.content === "") return; const currentUser = UserStore.getCurrentUser(); - if (message.author.id === currentUser.id) return; - if (await PresenceStore.getStatus(currentUser.id) !== "dnd") return; + const userStatus = await PresenceStore.getStatus(currentUser.id); + if (optimistic || message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || userStatus !== "dnd") { + return; + } const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && mentioned) { await showNotification(message, guildId); } else if (bypasses.users.includes(message.author.id)) { - if (channelId === await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id)) { - await showNotification(message); - } else if (mentioned && (settings.store.allowOutsideOfDms === true)) { + const userChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id); + if (channelId === userChannelId || (mentioned && settings.store.allowOutsideOfDms === true)) { await showNotification(message, guildId); } } From e1b75bdb573a307045052a081c2649d123a40a23 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 23:29:01 -0800 Subject: [PATCH 45/58] Removed DataStore shenanigans, this change means that bypasses can be saved to the cloud settings! --- src/plugins/bypassDND/index.tsx | 48 +++++++++------------------------ 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 9f7718eb62..de78d21bc2 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -5,7 +5,7 @@ */ import { type NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { DataStore, Notifications } from "@api/index"; +import { Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -13,7 +13,6 @@ import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, Priv import { type Message } from "discord-types/general"; interface IMessageCreate { - optimistic: boolean; channelId: string; guildId: string; message: Message; @@ -33,7 +32,7 @@ function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatc return (children, props) => { const type = props[name]; if (!type) return; - const enabled = bypasses[`${name}s`].includes(type.id); + const enabled = settings.store[`${name}s`].split(", ").includes(type.id); if (name === "user" && type.id === UserStore.getCurrentUser().id) return; children.splice(-1, 0, ( @@ -42,16 +41,10 @@ function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatc label={`${enabled ? "Remove" : "Add"} DND Bypass`} icon={() => icon(enabled)} action={() => { - if (enabled) bypasses[`${name}s`] = bypasses[`${name}s`].filter(id => id !== type.id); - else bypasses[`${name}s`].push(type.id); - - DataStore.set("bypassdnd", bypasses) - .then(() => { - settings.store[`${name}s`] = bypasses[`${name}s`].join(", "); - }) - .catch(error => { - console.error(error); - }); + let bypasses: string[] = settings.store[`${name}s`].split(", "); + if (enabled) bypasses = bypasses.filter(id => id !== type.id); + else bypasses.push(type.id); + settings.store[`${name}s`] = bypasses.join(", "); }} /> @@ -59,14 +52,6 @@ function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatc }; } -interface Bypasses { - guilds: string[]; - channels: string[]; - users: string[]; -} - -let bypasses: Bypasses; - const settings = definePluginSettings({ guilds: { type: OptionType.STRING, @@ -74,8 +59,7 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); - await DataStore.set("bypassdnd", bypasses); + settings.store.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); } }, channels: { @@ -84,8 +68,7 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); - await DataStore.set("bypassdnd", bypasses); + settings.store.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); } }, users: { @@ -94,8 +77,7 @@ const settings = definePluginSettings({ default: "", placeholder: "Separate with commas", onChange: async function (value) { - bypasses.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== ""); - await DataStore.set("bypassdnd", bypasses); + settings.store.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); } }, allowOutsideOfDms: { @@ -120,17 +102,17 @@ export default definePlugin({ description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", authors: [Devs.Inbestigator], flux: { - async MESSAGE_CREATE({ optimistic, message, guildId, channelId }: IMessageCreate) { + async MESSAGE_CREATE({ message, guildId, channelId }: IMessageCreate) { try { const currentUser = UserStore.getCurrentUser(); const userStatus = await PresenceStore.getStatus(currentUser.id); - if (optimistic || message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || userStatus !== "dnd") { + if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || userStatus !== "dnd") { return; } const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; - if ((bypasses.guilds.includes(guildId) || bypasses.channels.includes(channelId)) && mentioned) { + if ((settings.store.guilds.split(", ").includes(guildId) || settings.store.channels.split(", ").includes(channelId)) && mentioned) { await showNotification(message, guildId); - } else if (bypasses.users.includes(message.author.id)) { + } else if (settings.store.users.split(", ").includes(message.author.id)) { const userChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id); if (channelId === userChannelId || (mentioned && settings.store.allowOutsideOfDms === true)) { await showNotification(message, guildId); @@ -146,9 +128,5 @@ export default definePlugin({ "guild-context": ContextCallback("guild"), "channel-context": ContextCallback("channel"), "user-context": ContextCallback("user"), - }, - async start() { - bypasses = (await DataStore.get("bypassdnd")) ?? { guilds: [], channels: [], users: [] }; - await DataStore.set("bypassdnd", bypasses); } }); From 1e462cc18740267789d265db6d8b762cab598cf9 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 23:31:23 -0800 Subject: [PATCH 46/58] Shortbread --- src/plugins/bypassDND/index.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index de78d21bc2..2e6c5bc6b5 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -58,27 +58,21 @@ const settings = definePluginSettings({ description: "Guilds to let bypass (notified when pinged anywhere in guild)", default: "", placeholder: "Separate with commas", - onChange: async function (value) { - settings.store.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); - } + onChange: value => settings.store.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") }, channels: { type: OptionType.STRING, description: "Channels to let bypass (notified when pinged in that channel)", default: "", placeholder: "Separate with commas", - onChange: async function (value) { - settings.store.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); - } + onChange: value => settings.store.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") }, users: { type: OptionType.STRING, description: "Users to let bypass (notified for all messages sent in DMs)", default: "", placeholder: "Separate with commas", - onChange: async function (value) { - settings.store.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); - } + onChange: value => settings.store.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") }, allowOutsideOfDms: { type: OptionType.BOOLEAN, From fd9f7aafcf950bfd5814fd3ef244f8f1e6bc963c Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 8 Mar 2024 23:38:58 -0800 Subject: [PATCH 47/58] Welfare --- src/plugins/bypassDND/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 2e6c5bc6b5..823ba707d4 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -44,7 +44,7 @@ function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatc let bypasses: string[] = settings.store[`${name}s`].split(", "); if (enabled) bypasses = bypasses.filter(id => id !== type.id); else bypasses.push(type.id); - settings.store[`${name}s`] = bypasses.join(", "); + settings.store[`${name}s`] = bypasses.filter(id => id.trim() !== "").join(", "); }} /> From e483584effb82e5dc6e1e03da1b3bd04299bba31 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 9 Mar 2024 12:57:10 -0800 Subject: [PATCH 48/58] Organz (Organization) --- src/plugins/bypassDND/index.tsx | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 823ba707d4..bd2edf9ba6 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -28,6 +28,21 @@ function icon(enabled?: boolean) { ; } +function processIds(value) { + return value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); +} + +async function showNotification(message: Message, guildId?: string): Promise { + await Notifications.showNotification({ + title: `${message.author.globalName ?? message.author.username} ${guildId ? `sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}` : "sent a message in a DM"}`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + onClick: function () { + NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); + } + }); +} + function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatchCallback { return (children, props) => { const type = props[name]; @@ -58,21 +73,21 @@ const settings = definePluginSettings({ description: "Guilds to let bypass (notified when pinged anywhere in guild)", default: "", placeholder: "Separate with commas", - onChange: value => settings.store.guilds = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") + onChange: value => settings.store.guilds = processIds(value) }, channels: { type: OptionType.STRING, description: "Channels to let bypass (notified when pinged in that channel)", default: "", placeholder: "Separate with commas", - onChange: value => settings.store.channels = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") + onChange: value => settings.store.channels = processIds(value) }, users: { type: OptionType.STRING, description: "Users to let bypass (notified for all messages sent in DMs)", default: "", placeholder: "Separate with commas", - onChange: value => settings.store.users = value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", ") + onChange: value => settings.store.users = processIds(value) }, allowOutsideOfDms: { type: OptionType.BOOLEAN, @@ -80,17 +95,6 @@ const settings = definePluginSettings({ } }); -async function showNotification(message: Message, guildId?: string): Promise { - await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} ${guildId ? `sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}` : "sent a message in a DM"}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - onClick: function () { - NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); - } - }); -} - export default definePlugin({ name: "BypassDND", description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", From e331cfe52e8c35a19043963cc252fb15e7a52b4d Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 10 Mar 2024 20:52:07 -0700 Subject: [PATCH 49/58] Made it check focus/channel --- src/plugins/bypassDND/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index bd2edf9ba6..fa201ac8e8 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -8,8 +8,9 @@ import { type NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; +import { getCurrentChannel } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore } from "@webpack/common"; +import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore, WindowStore } from "@webpack/common"; import { type Message } from "discord-types/general"; interface IMessageCreate { @@ -104,7 +105,7 @@ export default definePlugin({ try { const currentUser = UserStore.getCurrentUser(); const userStatus = await PresenceStore.getStatus(currentUser.id); - if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || userStatus !== "dnd") { + if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === getCurrentChannel().id && WindowStore.isFocused()) || userStatus !== "dnd") { return; } const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; From 19fb542f20ad3ca2ae8c51f286842b4b057dab77 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 13 Mar 2024 14:40:22 -0700 Subject: [PATCH 50/58] Updated notification to look more like the normal notification --- src/plugins/bypassDND/index.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index fa201ac8e8..78279cadd9 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -33,9 +33,21 @@ function processIds(value) { return value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); } -async function showNotification(message: Message, guildId?: string): Promise { +async function showNotification(message, guildId) { + const channel = ChannelStore.getChannel(message.channel_id); + const channelRegex = /<#(\d{19})>/g; + const userRegex = /<@(\d{18})>/g; + + message.content = message.content.replace(channelRegex, (match, channelId) => { + return `#${ChannelStore.getChannel(channelId)?.name}`; + }); + + message.content = message.content.replace(userRegex, (match, userId) => { + return `@${UserStore.getUser(userId)?.globalName}`; + }); + await Notifications.showNotification({ - title: `${message.author.globalName ?? message.author.username} ${guildId ? `sent a message in ${ChannelStore.getChannel(message.channel_id)?.name}` : "sent a message in a DM"}`, + title: `${message.author.globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), onClick: function () { @@ -105,7 +117,8 @@ export default definePlugin({ try { const currentUser = UserStore.getCurrentUser(); const userStatus = await PresenceStore.getStatus(currentUser.id); - if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === getCurrentChannel().id && WindowStore.isFocused()) || userStatus !== "dnd") { + const currentChannelId = getCurrentChannel()?.id ?? "0"; + if (message.state === "SENDING" || message.content === "" || message.author.id === currentUser.id || (channelId === currentChannelId && WindowStore.isFocused()) || userStatus !== "dnd") { return; } const mentioned = MessageStore.getMessage(channelId, message.id)?.mentioned; From 175088ce2f99ab31023b9803d59051e0678ab3c4 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 13 Mar 2024 14:45:07 -0700 Subject: [PATCH 51/58] Added types --- src/plugins/bypassDND/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 78279cadd9..e9553be276 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -19,7 +19,7 @@ interface IMessageCreate { message: Message; } -function icon(enabled?: boolean) { +function icon(enabled?: boolean): JSX.Element { return ; } -function processIds(value) { +function processIds(value: string): string { return value.replace(/\s/g, "").split(",").filter(id => id.trim() !== "").join(", "); } -async function showNotification(message, guildId) { +async function showNotification(message: Message, guildId: string | undefined): Promise { const channel = ChannelStore.getChannel(message.channel_id); const channelRegex = /<#(\d{19})>/g; const userRegex = /<@(\d{18})>/g; - message.content = message.content.replace(channelRegex, (match, channelId) => { + message.content = message.content.replace(channelRegex, (match, channelId: string) => { return `#${ChannelStore.getChannel(channelId)?.name}`; }); - message.content = message.content.replace(userRegex, (match, userId) => { + message.content = message.content.replace(userRegex, (match, userId: string) => { return `@${UserStore.getUser(userId)?.globalName}`; }); @@ -50,7 +50,7 @@ async function showNotification(message, guildId) { title: `${message.author.globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - onClick: function () { + onClick: function (): void { NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); } }); @@ -113,7 +113,7 @@ export default definePlugin({ description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.", authors: [Devs.Inbestigator], flux: { - async MESSAGE_CREATE({ message, guildId, channelId }: IMessageCreate) { + async MESSAGE_CREATE({ message, guildId, channelId }: IMessageCreate): Promise { try { const currentUser = UserStore.getCurrentUser(); const userStatus = await PresenceStore.getStatus(currentUser.id); From 49777e4e5f9be0db102367f27986a62f576d8adf Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 13 Mar 2024 15:06:34 -0700 Subject: [PATCH 52/58] Made icon PascalCase --- src/plugins/bypassDND/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index e9553be276..71a0f0c957 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -19,7 +19,7 @@ interface IMessageCreate { message: Message; } -function icon(enabled?: boolean): JSX.Element { +function Icon(enabled?: boolean): JSX.Element { return icon(enabled)} + icon={() => Icon(enabled)} action={() => { let bypasses: string[] = settings.store[`${name}s`].split(", "); if (enabled) bypasses = bypasses.filter(id => id !== type.id); From 4875cb0dc409cf47ea1ac9632db10025b7dab428 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Wed, 13 Mar 2024 15:09:43 -0700 Subject: [PATCH 53/58] Changed to Logger --- src/plugins/bypassDND/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 71a0f0c957..5303de5cbd 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -9,6 +9,7 @@ import { Notifications } from "@api/index"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { getCurrentChannel } from "@utils/discord"; +import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore, WindowStore } from "@webpack/common"; import { type Message } from "discord-types/general"; @@ -131,7 +132,7 @@ export default definePlugin({ } } } catch (error) { - console.error(error); + new Logger("BypassDND").error("Failed to handle message", error); } } }, From 46063b42d891b5b2dcdeb426a156913959e29850 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sun, 17 Mar 2024 18:48:15 -0700 Subject: [PATCH 54/58] Moved self to bottom of devs --- src/utils/constants.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 06a908a9a2..d1db2516dd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -50,10 +50,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Vendicated", id: 343383572805058560n }, - Inbestigator: { - name: "Inbestigator", - id: 761777382041714690n - }, Arjix: { name: "ArjixWasTaken", id: 674710789138939916n @@ -430,6 +426,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Elvyra: { name: "Elvyra", id: 708275751816003615n, + }, + Inbestigator: { + name: "Inbestigator", + id: 761777382041714690n } } satisfies Record); From 107140a4c10cb59d8fc7ec0bbecc80152855343b Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 21 Mar 2024 21:03:07 -0700 Subject: [PATCH 55/58] Fixed using globalname --- src/plugins/bypassDND/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 5303de5cbd..c94b770260 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -44,11 +44,11 @@ async function showNotification(message: Message, guildId: string | undefined): }); message.content = message.content.replace(userRegex, (match, userId: string) => { - return `@${UserStore.getUser(userId)?.globalName}`; + return `@${(UserStore.getUser(userId) as any).globalName}`; }); await Notifications.showNotification({ - title: `${message.author.globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, + title: `${(message.author as any).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), onClick: function (): void { From 3636de84661ef780c33149f8fa2e647b1c642770 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 21 Mar 2024 21:28:09 -0700 Subject: [PATCH 56/58] Enforced User type --- src/plugins/bypassDND/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index c94b770260..8d6bd5248f 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -12,7 +12,11 @@ import { getCurrentChannel } from "@utils/discord"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore, WindowStore } from "@webpack/common"; -import { type Message } from "discord-types/general"; +import type { Message, User as DiscordUser } from "discord-types/general"; + +interface User extends DiscordUser { + globalName: string; +} interface IMessageCreate { channelId: string; @@ -44,11 +48,11 @@ async function showNotification(message: Message, guildId: string | undefined): }); message.content = message.content.replace(userRegex, (match, userId: string) => { - return `@${(UserStore.getUser(userId) as any).globalName}`; + return `@${(UserStore.getUser(userId) as User).globalName}`; }); await Notifications.showNotification({ - title: `${(message.author as any).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, + title: `${(message.author as User).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, body: message.content, icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), onClick: function (): void { From 172b754ab96a11d19523e27f00813d5b5f6472f8 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Fri, 12 Apr 2024 22:04:02 -0700 Subject: [PATCH 57/58] Added notification sound (thanks to Drakz for the snippet) --- src/plugins/bypassDND/index.tsx | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/plugins/bypassDND/index.tsx b/src/plugins/bypassDND/index.tsx index 8d6bd5248f..eb24465588 100644 --- a/src/plugins/bypassDND/index.tsx +++ b/src/plugins/bypassDND/index.tsx @@ -12,11 +12,7 @@ import { getCurrentChannel } from "@utils/discord"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, Menu, MessageStore, NavigationRouter, PresenceStore, PrivateChannelsStore, UserStore, WindowStore } from "@webpack/common"; -import type { Message, User as DiscordUser } from "discord-types/general"; - -interface User extends DiscordUser { - globalName: string; -} +import type { Message } from "discord-types/general"; interface IMessageCreate { channelId: string; @@ -39,26 +35,34 @@ function processIds(value: string): string { } async function showNotification(message: Message, guildId: string | undefined): Promise { - const channel = ChannelStore.getChannel(message.channel_id); - const channelRegex = /<#(\d{19})>/g; - const userRegex = /<@(\d{18})>/g; + try { + const channel = ChannelStore.getChannel(message.channel_id); + const channelRegex = /<#(\d{19})>/g; + const userRegex = /<@(\d{18})>/g; - message.content = message.content.replace(channelRegex, (match, channelId: string) => { - return `#${ChannelStore.getChannel(channelId)?.name}`; - }); + message.content = message.content.replace(channelRegex, (match: any, channelId: string) => { + return `#${ChannelStore.getChannel(channelId)?.name}`; + }); - message.content = message.content.replace(userRegex, (match, userId: string) => { - return `@${(UserStore.getUser(userId) as User).globalName}`; - }); + message.content = message.content.replace(userRegex, (match: any, userId: string) => { + return `@${(UserStore.getUser(userId) as any).globalName}`; + }); + + await Notifications.showNotification({ + title: `${(message.author as any).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, + body: message.content, + icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), + onClick: function (): void { + NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); + } + }); - await Notifications.showNotification({ - title: `${(message.author as User).globalName} ${guildId ? `(#${channel?.name}, ${ChannelStore.getChannel(channel?.parent_id)?.name})` : ""}`, - body: message.content, - icon: UserStore.getUser(message.author.id).getAvatarURL(undefined, undefined, false), - onClick: function (): void { - NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${message.channel_id}/${message.id}`); + if (settings.store.notificationSound) { + new Audio("https://discord.com/assets/9422aef94aa931248105.mp3").play(); } - }); + } catch (error) { + new Logger("BypassDND").error("Failed to notify user: ", error); + } } function ContextCallback(name: "guild" | "user" | "channel"): NavContextMenuPatchCallback { @@ -110,6 +114,11 @@ const settings = definePluginSettings({ allowOutsideOfDms: { type: OptionType.BOOLEAN, description: "Allow selected users to bypass DND outside of DMs too (acts like a channel/guild bypass, but it's for all messages sent by the selected users)" + }, + notificationSound: { + type: OptionType.BOOLEAN, + description: "Whether the notification sound should be played", + default: true, } }); @@ -136,7 +145,7 @@ export default definePlugin({ } } } catch (error) { - new Logger("BypassDND").error("Failed to handle message", error); + new Logger("BypassDND").error("Failed to handle message: ", error); } } }, From 75ef0eb73d4ec22904dd3fae1adef4a5be967967 Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Thu, 8 Aug 2024 03:02:19 -0700 Subject: [PATCH 58/58] Create README.md --- src/plugins/bypassDND/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/plugins/bypassDND/README.md diff --git a/src/plugins/bypassDND/README.md b/src/plugins/bypassDND/README.md new file mode 100644 index 0000000000..8e53f16254 --- /dev/null +++ b/src/plugins/bypassDND/README.md @@ -0,0 +1,7 @@ +# BypassDND + +Bypass DND allows you to set specific guilds/channels/users to still receive a notification from when in DND mode. +You will only get the notification if: + +- The message is pinging you (for guild and channel bypasses, or users too if `allowOutsideOfDms` is enabled) +- All messages in DMs