Add DevCompanion plugin (https://github.com/Vencord/Companion)
This commit is contained in:
		
							parent
							
								
									d5c05d857f
								
							
						
					
					
						commit
						983414d024
					
				
					 1 changed files with 250 additions and 0 deletions
				
			
		
							
								
								
									
										250
									
								
								src/plugins/devCompanion.dev.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/plugins/devCompanion.dev.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,250 @@ | |||
| /* | ||||
|  * Vencord, a modification for Discord's desktop app | ||||
|  * Copyright (c) 2023 Vendicated and contributors | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
| */ | ||||
| 
 | ||||
| import { addContextMenuPatch } from "@api/ContextMenu"; | ||||
| import { showNotification } from "@api/Notifications"; | ||||
| import { Devs } from "@utils/constants"; | ||||
| import Logger from "@utils/Logger"; | ||||
| import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; | ||||
| import definePlugin from "@utils/types"; | ||||
| import { filters, findAll, search } from "@webpack"; | ||||
| import { Menu } from "@webpack/common"; | ||||
| 
 | ||||
| const PORT = 8485; | ||||
| const NAV_ID = "dev-companion-reconnect"; | ||||
| 
 | ||||
| const logger = new Logger("DevCompanion"); | ||||
| 
 | ||||
| let socket: WebSocket | undefined; | ||||
| 
 | ||||
| type Node = StringNode | RegexNode | FunctionNode; | ||||
| 
 | ||||
| interface StringNode { | ||||
|     type: "string"; | ||||
|     value: string; | ||||
| } | ||||
| 
 | ||||
| interface RegexNode { | ||||
|     type: "regex"; | ||||
|     value: { | ||||
|         pattern: string; | ||||
|         flags: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| interface FunctionNode { | ||||
|     type: "function"; | ||||
|     value: string; | ||||
| } | ||||
| 
 | ||||
| interface PatchData { | ||||
|     find: string; | ||||
|     replacement: { | ||||
|         match: StringNode | RegexNode; | ||||
|         replace: StringNode | FunctionNode; | ||||
|     }[]; | ||||
| } | ||||
| 
 | ||||
| interface FindData { | ||||
|     type: string; | ||||
|     args: Array<StringNode | FunctionNode>; | ||||
| } | ||||
| 
 | ||||
| function parseNode(node: Node) { | ||||
|     switch (node.type) { | ||||
|         case "string": | ||||
|             return node.value; | ||||
|         case "regex": | ||||
|             return new RegExp(node.value.pattern, node.value.flags); | ||||
|         case "function": | ||||
|             // We LOVE remote code execution
 | ||||
|             // Safety: This comes from localhost only, which actually means we have less permissions than the source,
 | ||||
|             // since we're running in the browser sandbox, whereas the sender has host access
 | ||||
|             return (0, eval)(node.value); | ||||
|         default: | ||||
|             throw new Error("Unknown Node Type " + (node as any).type); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function initWs(isManual = false) { | ||||
|     let wasConnected = isManual; | ||||
|     let hasErrored = false; | ||||
|     const ws = socket = new WebSocket(`ws://localhost:${PORT}`); | ||||
| 
 | ||||
|     ws.addEventListener("open", () => { | ||||
|         wasConnected = true; | ||||
| 
 | ||||
|         logger.info("Connected to WebSocket"); | ||||
| 
 | ||||
|         showNotification({ | ||||
|             title: "Dev Companion Connected", | ||||
|             body: "Connected to WebSocket" | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     ws.addEventListener("error", e => { | ||||
|         if (!wasConnected) return; | ||||
| 
 | ||||
|         hasErrored = true; | ||||
| 
 | ||||
|         logger.error("Dev Companion Error:", e); | ||||
| 
 | ||||
|         showNotification({ | ||||
|             title: "Dev Companion Error", | ||||
|             body: (e as ErrorEvent).message || "No Error Message", | ||||
|             color: "var(--status-danger, red)" | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     ws.addEventListener("close", e => { | ||||
|         if (!wasConnected && !hasErrored) return; | ||||
| 
 | ||||
|         logger.info("Dev Companion Disconnected:", e.code, e.reason); | ||||
| 
 | ||||
|         showNotification({ | ||||
|             title: "Dev Companion Disconnected", | ||||
|             body: e.reason || "No Reason provided", | ||||
|             color: "var(--status-danger, red)" | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     ws.addEventListener("message", e => { | ||||
|         try { | ||||
|             var { nonce, type, data } = JSON.parse(e.data); | ||||
|         } catch (err) { | ||||
|             logger.error("Invalid JSON:", err, "\n" + e.data); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         function reply(error?: string) { | ||||
|             const data = { nonce, ok: !error } as Record<string, unknown>; | ||||
|             if (error) data.error = error; | ||||
| 
 | ||||
|             ws.send(JSON.stringify(data)); | ||||
|         } | ||||
| 
 | ||||
|         logger.info("Received Message:", type, "\n", data); | ||||
| 
 | ||||
|         switch (type) { | ||||
|             case "testPatch": { | ||||
|                 const { find, replacement } = data as PatchData; | ||||
| 
 | ||||
|                 const candidates = search(find); | ||||
|                 const keys = Object.keys(candidates); | ||||
|                 if (keys.length !== 1) | ||||
|                     return reply("Expected exactly one 'find' matches, found " + keys.length); | ||||
| 
 | ||||
|                 let src = String(candidates[keys[0]]); | ||||
| 
 | ||||
|                 let i = 0; | ||||
| 
 | ||||
|                 for (const { match, replace } of replacement) { | ||||
|                     i++; | ||||
| 
 | ||||
|                     try { | ||||
|                         const matcher = canonicalizeMatch(parseNode(match)); | ||||
|                         const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); | ||||
| 
 | ||||
|                         const newSource = src.replace(matcher, replacement as string); | ||||
| 
 | ||||
|                         if (src === newSource) throw "Had no effect"; | ||||
|                         Function(newSource); | ||||
| 
 | ||||
|                         src = newSource; | ||||
|                     } catch (err) { | ||||
|                         return reply(`Replacement ${i} failed: ${err}`); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 reply(); | ||||
|                 break; | ||||
|             } | ||||
|             case "testFind": { | ||||
|                 const { type, args } = data as FindData; | ||||
|                 try { | ||||
|                     var parsedArgs = args.map(parseNode); | ||||
|                 } catch (err) { | ||||
|                     return reply("Failed to parse args: " + err); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     let results: any[]; | ||||
|                     switch (type.replace("find", "").replace("Lazy", "")) { | ||||
|                         case "": | ||||
|                             results = findAll(parsedArgs[0]); | ||||
|                             break; | ||||
|                         case "ByProps": | ||||
|                             results = findAll(filters.byProps(...parsedArgs)); | ||||
|                             break; | ||||
|                         case "Store": | ||||
|                             results = findAll(filters.byStoreName(parsedArgs[0])); | ||||
|                             break; | ||||
|                         case "ByCode": | ||||
|                             results = findAll(filters.byCode(...parsedArgs)); | ||||
|                             break; | ||||
|                         case "ModuleId": | ||||
|                             results = Object.keys(search(parsedArgs[0])); | ||||
|                             break; | ||||
|                         default: | ||||
|                             return reply("Unknown Find Type " + type); | ||||
|                     } | ||||
| 
 | ||||
|                     if (results.length === 0) throw "No results"; | ||||
|                     if (results.length > 1) throw "Found more than one result! Make this filter more specific"; | ||||
|                 } catch (err) { | ||||
|                     return reply("Failed to find: " + err); | ||||
|                 } | ||||
| 
 | ||||
|                 reply(); | ||||
|                 break; | ||||
|             } | ||||
|             default: | ||||
|                 reply("Unknown Type " + type); | ||||
|                 break; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export default definePlugin({ | ||||
|     name: "DevCompanion", | ||||
|     description: "Dev Companion Plugin", | ||||
|     authors: [Devs.Ven], | ||||
| 
 | ||||
|     start() { | ||||
|         initWs(); | ||||
|         addContextMenuPatch("user-settings-cog", kids => { | ||||
|             if (kids.some(k => k?.props?.id === NAV_ID)) return; | ||||
| 
 | ||||
|             kids.unshift( | ||||
|                 <Menu.MenuItem | ||||
|                     id={NAV_ID} | ||||
|                     label="Reconnect Dev Companion" | ||||
|                     action={() => { | ||||
|                         socket?.close(1000, "Reconnecting"); | ||||
|                         initWs(true); | ||||
|                     }} | ||||
|                 /> | ||||
|             ); | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     stop() { | ||||
|         socket?.close(1000, "Plugin Stopped"); | ||||
|         socket = void 0; | ||||
|     } | ||||
| }); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue