diff --git a/package.json b/package.json index dde55d311..13017b792 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 43866f50b..a0cc197b9 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 000000000..0ec99cc85 --- /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); +}