diff --git a/README.md b/README.md index c8d7f47..8f01108 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - **Various mods built in** - Enjoy Cumcord, GooseMod, Flicker, and their many features, or have a more vanilla experience, it's your choice! + Enjoy Vencord, Shelter and their many features, or have a more vanilla experience, it's your choice! - **Made for Privacy™** diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..be8622e --- /dev/null +++ b/log.txt @@ -0,0 +1,146 @@ + +> ArmCord@3.1.0 start +> npm run build && electron ./ts-out/main.js + + +> ArmCord@3.1.0 build +> tsc && copyfiles -u 1 src/**/*.html src/**/**/*.css ts-out/ && copyfiles package.json ts-out/ && copyfiles assets/**/** ts-out/ + +[Config manager] doneSetup: undefined +[Config manager] performanceMode: none +ArmCord has been run before. Skipping setup. +No performance modes set +[Config manager] windowStyle: default +[Config manager] armcordCSP: true +[Config manager] doneSetup: undefined +[Config manager] customIcon: undefined +Setting up CSP unstricter... +[Config manager] trayIcon: default +[Config manager] windowStyle: default +[Config manager] windowStyle: default +[Config manager] ignoreProtocolWarning: undefined +[Config manager] clientName: undefined +[Config manager] 0: undefined +[Config manager] mods: vencord +Downloading mod bundle +[Config manager] mods: vencord +[Config manager] mobileMode: false +[Config manager] trayIcon: default +[Mod loader] Loaded ArmCord Mod Loader made by Vendicated +[Config manager] alternativePaste: false +undefined +[Config manager] inviteWebsocket: true +[Config manager] skipSplash: undefined +[arRPC > ipc] checking /run/user/1000/discord-ipc-0 +Error: connect ECONNREFUSED /run/user/1000/discord-ipc-0 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -111, + code: 'ECONNREFUSED', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-0' +} +[Config manager] channel: stable +[Config manager] mods: vencord +[Config manager] automaticPatches: false +[Config manager] channel: stable +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 1) +[arRPC > ipc] checking /run/user/1000/discord-ipc-1 +Error: connect ENOENT /run/user/1000/discord-ipc-1 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-1' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 2) +[arRPC > ipc] checking /run/user/1000/discord-ipc-2 +Error: connect ENOENT /run/user/1000/discord-ipc-2 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-2' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 3) +[arRPC > ipc] checking /run/user/1000/discord-ipc-3 +Error: connect ENOENT /run/user/1000/discord-ipc-3 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-3' +} +[Window state manager] width: 800 +[Window state manager] height: 600 +[Window state manager] isMaximized: false +[Window state manager] Not maximized. +[Config manager] channel: stable +[Config manager] mods: vencord +[Config manager] automaticPatches: false +[Config manager] channel: stable +[Config manager] mobileMode: false +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 4) +[arRPC > ipc] checking /run/user/1000/discord-ipc-4 +Error: connect ENOENT /run/user/1000/discord-ipc-4 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-4' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 5) +[arRPC > ipc] checking /run/user/1000/discord-ipc-5 +Error: connect ENOENT /run/user/1000/discord-ipc-5 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-5' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 6) +[arRPC > ipc] checking /run/user/1000/discord-ipc-6 +Error: connect ENOENT /run/user/1000/discord-ipc-6 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-6' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 7) +[arRPC > ipc] checking /run/user/1000/discord-ipc-7 +Error: connect ENOENT /run/user/1000/discord-ipc-7 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-7' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 8) +[arRPC > ipc] checking /run/user/1000/discord-ipc-8 +Error: connect ENOENT /run/user/1000/discord-ipc-8 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-8' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 9) +[arRPC > ipc] checking /run/user/1000/discord-ipc-9 +Error: connect ENOENT /run/user/1000/discord-ipc-9 + at PipeConnectWrap.afterConnect [as oncomplete] (node:net:1187:16) { + errno: -2, + code: 'ENOENT', + syscall: 'connect', + address: '/run/user/1000/discord-ipc-9' +} +[arRPC > ipc] checked if socket is available: false - reason: timed out +[arRPC > ipc] not available, trying again (attempt 10) diff --git a/package.json b/package.json index 98ed1b3..905962c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "electron-context-menu": "github:ArmCord/electron-context-menu", "extract-zip": "^2.0.1", "node-fetch": "v2", + "arrpc": "file:./src/arrpc", "os-locale": "^6.0.2", "v8-compile-cache": "^2.3.0", "ws": "^8.8.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4dd7f3..671badb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ specifiers: '@types/node': ^17.0.42 '@types/node-fetch': ^2.6.2 '@types/ws': ^8.5.3 + arrpc: file:./src/arrpc chalk-cli: ^5.0.0 copyfiles: ^2.4.1 electron: ^20.1.0 @@ -21,6 +22,7 @@ specifiers: dependencies: '@pyke/vibe': github.com/pykeio/vibe/11984868ce9e007859ed91ff159c7f7f0a34e7ae_electron@20.3.1 + arrpc: file:src/arrpc electron-context-menu: github.com/ArmCord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32 extract-zip: 2.0.1 node-fetch: 2.6.7 @@ -2188,6 +2190,19 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws/8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws/8.9.0: resolution: {integrity: sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==} engines: {node: '>=10.0.0'} @@ -2267,6 +2282,17 @@ packages: engines: {node: '>=10'} dev: true + file:src/arrpc: + resolution: {directory: src/arrpc, type: directory} + name: arrpc + version: 0.1.0 + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + github.com/ArmCord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32: resolution: {tarball: https://codeload.github.com/ArmCord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32} name: electron-context-menu diff --git a/src/arrpc/.gitignore b/src/arrpc/.gitignore new file mode 100644 index 0000000..25c8fdb --- /dev/null +++ b/src/arrpc/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/src/arrpc/LICENSE b/src/arrpc/LICENSE new file mode 100644 index 0000000..f5617f1 --- /dev/null +++ b/src/arrpc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 OpenAsar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/arrpc/README.md b/src/arrpc/README.md new file mode 100644 index 0000000..3106e64 --- /dev/null +++ b/src/arrpc/README.md @@ -0,0 +1,44 @@ +# arRPC + +arRPC is an open source implementation of Discord's half-documented local RPC servers for their desktop client. This open source implementation purely in NodeJS allows it to be used in many places where it is otherwise impossible to do: Discord web and alternative clients like Armcord/etc. It opens a simple bridge WebSocket server which messages the JSON of exactly what to dispatch with in the client with no extra processing needed, allowing small and simple mods or plugins. **It is currently in alpha and is very WIP, expect bugs, etc.** + +### How to try + +1. Clone repo +2. Run server with `node src` (use new Node) +3. Open Discord in browser with CSP disabled (using an extension) +4. Run content of [`simple_mod.js`](simple_mod.js) in console +5. Use an app/thing with RPC +6. Hope it works, if not report bugs :) + +## Supported + +### Transports + +- [x] WebSocket Server + - [x] JSON + - [ ] Erlpack +- [ ] HTTP Server +- [x] IPC + +### Commands + +- [x] DISPATCH +- [ ] AUTHORIZE +- [ ] AUTHENTICATE +- [ ] GET_GUILD +- [ ] GET_GUILDS +- [ ] GET_CHANNEL +- [ ] GET_CHANNELS +- [ ] SUBSCRIBE +- [ ] UNSUBSCRIBE +- [ ] SET_USER_VOICE_SETTINGS +- [ ] SELECT_VOICE_CHANNEL +- [ ] GET_SELECTED_VOICE_CHANNEL +- [ ] SELECT_TEXT_CHANNEL +- [ ] GET_VOICE_SETTINGS +- [ ] SET_VOICE_SETTINGS +- [ ] SET_CERTIFIED_DEVICES +- [x] SET_ACTIVITY +- [ ] SEND_ACTIVITY_JOIN_INVITE +- [ ] CLOSE_ACTIVITY_REQUEST diff --git a/src/arrpc/package.json b/src/arrpc/package.json new file mode 100644 index 0000000..ca74963 --- /dev/null +++ b/src/arrpc/package.json @@ -0,0 +1,22 @@ +{ + "name": "arrpc", + "version": "0.1.0", + "description": "Open Discord RPC server for atypical setups", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/OpenAsar/arrpc.git" + }, + "author": "OpenAsar", + "license": "MIT", + "bugs": { + "url": "https://github.com/OpenAsar/arrpc/issues" + }, + "homepage": "https://github.com/OpenAsar/arrpc#readme", + "dependencies": { + "ws": "^8.11.0" + } +} diff --git a/src/arrpc/simple_mod.js b/src/arrpc/simple_mod.js new file mode 100644 index 0000000..5b4bc8b --- /dev/null +++ b/src/arrpc/simple_mod.js @@ -0,0 +1,31 @@ +const dispatch = (() => { + let Dispatcher; + + return function (event) { + Dispatcher ??= window.Vencord?.Webpack.Common.FluxDispatcher; + if (!Dispatcher) { + const cache = webpackChunkdiscord_app.push([[Symbol()], {}, (w) => w]).c; + webpackChunkdiscord_app.pop(); + + outer: for (const id in cache) { + const mod = cache[id].exports; + for (const exp in mod) { + if (mod[exp]?.isDispatching) { + Dispatcher = mod[exp]; + break outer; + } + } + } + } + if (!Dispatcher) return; // failed to find, your choice if and how u wanna handle this + + return Dispatcher.dispatch(event); + }; +})(); +const ws = new WebSocket("ws://localhost:1337"); // connect to arRPC bridge +ws.onmessage = (x) => { + msg = JSON.parse(x.data); + console.log(msg); + + dispatch({type: "LOCAL_ACTIVITY_UPDATE", ...msg}); // set RPC status +}; diff --git a/src/arrpc/src/bridge.js b/src/arrpc/src/bridge.js new file mode 100644 index 0000000..2e7d82f --- /dev/null +++ b/src/arrpc/src/bridge.js @@ -0,0 +1,8 @@ +const ws = require("ws"); +const send = (msg) => { + wss.clients.forEach((x) => x.send(JSON.stringify(msg))); +}; + +const wss = new ws.WebSocketServer({port: 1337}); + +module.exports = {send}; diff --git a/src/arrpc/src/index.js b/src/arrpc/src/index.js new file mode 100644 index 0000000..b8d34f8 --- /dev/null +++ b/src/arrpc/src/index.js @@ -0,0 +1,11 @@ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : {default: mod}; + }; +const server = require("./server.js"); +global.fetch = __importDefault(require("node-fetch")); +async function start() { + const x = await new server.RPCServer(); +} +start(); diff --git a/src/arrpc/src/server.js b/src/arrpc/src/server.js new file mode 100644 index 0000000..4a4a782 --- /dev/null +++ b/src/arrpc/src/server.js @@ -0,0 +1,82 @@ +const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`; +const log = (...args) => console.log(`[${rgb(88, 101, 242, "arRPC")} > ${rgb(87, 242, 135, "bridge")}]`, ...args); + +const IPCServer = require("./transports/ipc.js"); + +const WSServer = require("./transports/websocket.js"); +const Bridge = require("./bridge.js"); + +const lookupAsset = (name, assets) => { + return assets.find((x) => x.name === name)?.id; +}; +class RPCServer { + constructor() { + return (async () => { + this.onConnection = this.onConnection.bind(this); + this.onMessage = this.onMessage.bind(this); + + this.ipc = await new IPCServer.IPCServer(this.onMessage, this.onConnection); + this.ws = await new WSServer.WSServer(this.onMessage, this.onConnection); + })(); + } + + onConnection(socket) { + socket.send({ + cmd: "DISPATCH", + evt: "READY", + data: {} + }); + } + + async onMessage(socket, {cmd, args}) { + switch (cmd) { + case "SET_ACTIVITY": + if (!socket.application) { + socket.application = await ( + await fetch(`https://discord.com/api/v9/oauth2/applications/${socket.clientId}/rpc`) + ).json(); + socket.application.assets = await ( + await fetch(`https://discord.com/api/v9/oauth2/applications/${socket.clientId}/assets`) + ).json(); + log("fetched app info for", socket.clientId, socket.application); + } + + const {activity, pid} = args; + const {buttons, timestamps, instance} = activity; + + const metadata = {}; + const extra = {}; + if (buttons) { + metadata.button_urls = buttons.map((x) => x.url); + extra.buttons = buttons.map((x) => x.label); + } + + if (timestamps) + for (const x in timestamps) { + if (Date.now().toString().length - timestamps[x].toString().length > 2) + timestamps[x] = Math.floor(1e3 * timestamps[x]); + } + + if (activity.assets?.large_image) + activity.assets.large_image = lookupAsset(activity.assets.large_image, socket.application.assets); + if (activity.assets?.small_image) + activity.assets.small_image = lookupAsset(activity.assets.small_image, socket.application.assets); + + Bridge.send({ + activity: { + name: socket.application.name, + application_id: socket.application.id, + type: 0, + metadata, + flags: instance ? 1 << 0 : 0, + ...activity, + ...extra + }, + pid + }); + + break; + } + } +} +module.exports = {RPCServer}; diff --git a/src/arrpc/src/transports/ipc.js b/src/arrpc/src/transports/ipc.js new file mode 100644 index 0000000..3ef8fb4 --- /dev/null +++ b/src/arrpc/src/transports/ipc.js @@ -0,0 +1,255 @@ +const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`; +const log = (...args) => console.log(`[${rgb(88, 101, 242, "arRPC")} > ${rgb(254, 231, 92, "ipc")}]`, ...args); +const path = require("path"); +const {platform, env} = require("process"); +const {unlinkSync} = require("fs"); +const {createServer, createConnection} = require("net"); + +const SOCKET_PATH = + platform === "win32" + ? "\\\\?\\pipe\\discord-ipc" + : path.join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || "/tmp", "discord-ipc"); + +const Types = { + HANDSHAKE: 0, + FRAME: 1, + CLOSE: 2, + PING: 3, + PONG: 4 +}; + +const CloseCodes = { + CLOSE_NORMAL: 1000, + CLOSE_UNSUPPORTED: 1003, + CLOSE_ABNORMAL: 1006 +}; + +const ErrorCodes = { + INVALID_CLIENTID: 4000, + INVALID_ORIGIN: 4001, + RATELIMITED: 4002, + TOKEN_REVOKED: 4003, + INVALID_VERSION: 4004, + INVALID_ENCODING: 4005 +}; + +let uniqueId = 0; + +const encode = (type, data) => { + data = JSON.stringify(data); + const dataSize = Buffer.byteLength(data); + + const buf = Buffer.alloc(dataSize + 8); + buf.writeInt32LE(type, 0); // type + buf.writeInt32LE(dataSize, 4); // data size + buf.write(data, 8, dataSize); // data + + return buf; +}; + +const read = (socket) => { + let resp = socket.read(8); + if (!resp) return; + + resp = Buffer.from(resp); + const type = resp.readInt32LE(0); + const dataSize = resp.readInt32LE(4); + + if (type < 0 || type >= Object.keys(Types).length) throw new Error("invalid type"); + + let data = socket.read(dataSize); + if (!data) throw new Error("failed reading data"); + + data = JSON.parse(Buffer.from(data).toString()); + + switch (type) { + case Types.PING: + socket.emit("ping", data); + socket.write(encode(Types.PONG, data)); + break; + + case Types.PONG: + socket.emit("pong", data); + break; + + case Types.HANDSHAKE: + if (socket._handshook) throw new Error("already handshook"); + + socket._handshook = true; + socket.emit("handshake", data); + break; + + case Types.FRAME: + if (!socket._handshook) throw new Error("need to handshake first"); + + socket.emit("request", data); + break; + + case Types.CLOSE: + socket.end(); + socket.destroy(); + break; + } + + read(socket); +}; + +const socketIsAvailable = async (socket) => { + socket.pause(); + socket.on("readable", () => { + try { + read(socket); + } catch (e) { + log("error whilst reading", e); + + socket.end( + encode(Types.CLOSE, { + code: CloseCodes.CLOSE_UNSUPPORTED, + message: e.message + }) + ); + socket.destroy(); + } + }); + + const stop = () => { + try { + socket.end(); + socket.destroy(); + } catch {} + }; + + const possibleOutcomes = Promise.race([ + new Promise((res) => socket.on("error", res)), // errore + new Promise((res, rej) => socket.on("pong", () => rej("socket ponged"))), // ponged + new Promise((res, rej) => setTimeout(() => rej("timed out"), 1000)) // timed out + ]).then( + () => true, + (e) => e + ); + + socket.write(encode(Types.PING, ++uniqueId)); + + const outcome = await possibleOutcomes; + stop(); + log("checked if socket is available:", outcome === true, outcome === true ? "" : `- reason: ${outcome}`); + + return outcome === true; +}; + +const getAvailableSocket = async (tries = 0) => { + if (tries > 9) { + throw new Error("ran out of tries to find socket", tries); + } + + const path = SOCKET_PATH + "-" + tries; + const socket = createConnection(path); + + log("checking", path); + + if (await socketIsAvailable(socket)) { + if (platform !== "win32") + try { + unlinkSync(path); + } catch {} + + return path; + } + + log(`not available, trying again (attempt ${tries + 1})`); + return getAvailableSocket(tries + 1); +}; + +class IPCServer { + constructor(messageHandler, connectionHandler) { + return new Promise(async (res) => { + this.messageHandler = messageHandler; + this.connectionHandler = connectionHandler; + + this.onConnection = this.onConnection.bind(this); + this.onMessage = this.onMessage.bind(this); + + const server = createServer(this.onConnection); + server.on("error", (e) => { + log("server error", e); + }); + + const socketPath = await getAvailableSocket(); + server.listen(socketPath, () => { + log("listening at", socketPath); + this.server = server; + + res(this); + }); + }); + } + + onConnection(socket) { + log("new connection!"); + + socket.pause(); + socket.on("readable", () => { + try { + read(socket); + } catch (e) { + log("error whilst reading", e); + + socket.end( + encode(Types.CLOSE, { + code: CloseCodes.CLOSE_UNSUPPORTED, + message: e.message + }) + ); + socket.destroy(); + } + }); + + socket.once("handshake", (params) => { + log("handshake:", params); + + const ver = params.v ?? 1; + const clientId = params.client_id ?? ""; + + if (ver !== 1) { + log("unsupported version requested", ver); + + socket.close(ErrorCodes.INVALID_VERSION); + return; + } + + if (clientId === "") { + log("client id required"); + + socket.close(ErrorCodes.INVALID_CLIENTID); + return; + } + + socket.on("error", (e) => { + log("socket error", e); + }); + + socket.on("close", (e) => { + log("socket closed", e); + }); + + socket.on("request", this.onMessage.bind(this, socket)); + + socket._send = socket.send; + socket.send = (msg) => { + log("sending", msg); + socket.write(encode(Types.FRAME, msg)); + }; + + socket.clientId = clientId; + + this.connectionHandler(socket); + }); + } + + onMessage(socket, msg) { + log("message", msg); + this.messageHandler(socket, msg); + } +} + +module.exports = {IPCServer}; diff --git a/src/arrpc/src/transports/websocket.js b/src/arrpc/src/transports/websocket.js new file mode 100644 index 0000000..4de9402 --- /dev/null +++ b/src/arrpc/src/transports/websocket.js @@ -0,0 +1,124 @@ +const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`; +const log = (...args) => console.log(`[${rgb(88, 101, 242, "arRPC")} > ${rgb(235, 69, 158, "websocket")}]`, ...args); +const ws = require("ws"); +const {createServer} = require("http"); +const querystring = require("querystring"); + +const portRange = [6463, 6472]; + +class WSServer { + constructor(messageHandler, connectionHandler) { + return new Promise(async (res) => { + this.messageHandler = messageHandler; + this.connectionHandler = connectionHandler; + + this.onConnection = this.onConnection.bind(this); + this.onMessage = this.onMessage.bind(this); + + let port = portRange[0]; + + let http, wss; + while (port <= portRange[1]) { + try { + log("trying port", port); + + http = createServer(); + http.on("error", (e) => { + log("http error", e); + + if (e.code === "EADDRINUSE") { + log(port, "in use!"); + } + }); + + wss = new ws.WebSocketServer({server: http}); + wss.on("error", (e) => { + log("wss error", e); + }); + + wss.on("connection", this.onConnection); + + http.listen(port, "127.0.0.1", () => { + log("listening on", port); + + this.http = http; + this.wss = wss; + + res(this); + }); + } catch (e) { + log("failed to start", e); + } + + break; + } + }); + } + + onConnection(socket, req) { + const params = querystring.parse(req.url.split("?")[1]); + const ver = parseInt(params.v ?? 1); + const encoding = params.encoding ?? "json"; + const clientId = params.client_id ?? ""; + + const origin = req.headers.origin ?? ""; + + log(`new connection! origin:`, origin, JSON.parse(JSON.stringify(params))); + + if (origin !== "") { + log("origin is defined, denying", origin); + + socket.close(); + return; + } + + if (encoding !== "json") { + log("unsupported encoding requested", encoding); + + socket.close(); + return; + } + + if (ver !== 1) { + log("unsupported version requested", ver); + + socket.close(); + return; + } + + if (clientId === "") { + log("client id required"); + + socket.close(); + return; + } + + socket.clientId = clientId; + socket.encoding = encoding; + + socket.on("error", (e) => { + log("socket error", e); + }); + + socket.on("close", (e, r) => { + log("socket closed", e); + }); + + socket.on("message", this.onMessage.bind(this, socket)); + + socket._send = socket.send; + socket.send = (msg) => { + log("sending", msg); + socket._send(JSON.stringify(msg)); + }; + + this.connectionHandler(socket); + } + + onMessage(socket, msg) { + log("message", JSON.parse(msg)); + this.messageHandler(socket, JSON.parse(msg)); + } +} + +module.exports = {WSServer}; diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 8ab16e2..7536f3c 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -5,7 +5,7 @@ import "./patch"; import * as fs from "fs"; import * as path from "path"; import {injectHummusTitlebar, injectTitlebar} from "./titlebar"; -import {sleep, addStyle} from "../utils"; +import {sleep, addStyle, addScript} from "../utils"; import {injectMobileStuff} from "./mobile"; var version = ipcRenderer.sendSync("displayVersion"); var channel = ipcRenderer.sendSync("channel"); @@ -42,6 +42,41 @@ if (window.location.href.indexOf("splash.html") > -1) { injectMobileStuff(); } sleep(5000).then(async () => { + addScript(` + const dispatch = (() => { + let Dispatcher; + + return function (event) { + Dispatcher ??= window.Vencord?.Webpack.Common.FluxDispatcher + if (!Dispatcher) { + const cache = webpackChunkdiscord_app.push([[Symbol()], {}, w => w]).c; + webpackChunkdiscord_app.pop() + + outer: + for (const id in cache) { + const mod = cache[id].exports; + for (const exp in mod) { + if (mod[exp]?.isDispatching) { + Dispatcher = mod[exp]; + break outer; + } + } + } + } + if (!Dispatcher) + return; // failed to find, your choice if and how u wanna handle this + + return Dispatcher.dispatch(event); + }; + })(); + const ws = new WebSocket('ws://localhost:1337'); // connect to arRPC bridge + ws.onmessage = x => { + msg = JSON.parse(x.data); + console.log(msg); + + dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...msg }); // set RPC status + }; + `); const cssPath = path.join(__dirname, "../", "/content/css/discord.css"); addStyle(fs.readFileSync(cssPath, "utf8")); await updateLang(); diff --git a/src/window.ts b/src/window.ts index e2fc8fe..94f8c8a 100644 --- a/src/window.ts +++ b/src/window.ts @@ -205,7 +205,9 @@ async function doAfterDefiningTheWindow() { }); console.log(contentPath); if ((await getConfig("inviteWebsocket")) == true) { - await startServer(); + //@ts-ignore + import("arrpc") + //await startServer(); } if (firstRun) { await setLang(Intl.DateTimeFormat().resolvedOptions().locale);