diff --git a/src/arrpc/src/index.cjs b/src/arrpc/src/index.cjs index 8ab51c4..93d7943 100644 --- a/src/arrpc/src/index.cjs +++ b/src/arrpc/src/index.cjs @@ -11,13 +11,8 @@ async function run() { server.on("activity", (data) => mainWindow.webContents.send("rpc", data)); server.on("invite", (code) => { console.log(code); - const {createInviteWindow, inviteWindow} = require("../../../ts-out/window.js"); - const {exportPort} = require("./transports/websocket.js"); - createInviteWindow(); - const win = inviteWindow; - //doesnt work - win.loadURL("https://discord.com/invite/" + code); - win.show(); + const {createInviteWindow} = require("../../../ts-out/window.js"); + createInviteWindow(code); }); } run(); diff --git a/src/socket.ts b/src/socket.ts deleted file mode 100644 index 4b6565b..0000000 --- a/src/socket.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type {Server, WebSocket} from "ws"; -import {inviteWindow, createInviteWindow} from "./window"; -/* - MIT License - - Copyright (c) 2020-2022 Dawid Papiewski "SpacingBat3" - - 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. - */ -async function wsLog(message: string, ...args: unknown[]) { - console.log("[WebSocket] " + message, ...args); -} - -/** Generates an inclusive range (as `Array`) from `start` to `end`. */ -function range(start: number, end: number) { - return Array.from({length: end - start + 1}, (_v, k) => start + k); -} - -interface InviteResponse { - /** Response type/command. */ - cmd: "INVITE_BROWSER"; - /** Response arguments. */ - args: { - /** An invitation code. */ - code: string; - }; - /** Nonce indentifying the communication. */ - nonce: string; -} - -function isInviteResponse(data: unknown): data is InviteResponse { - if (!(data instanceof Object)) return false; - if ((data as Partial)?.cmd !== "INVITE_BROWSER") return false; - if (typeof (data as Partial)?.args?.code !== "string") return false; - if (typeof (data as Partial)?.nonce !== "string") return false; - return true; -} - -const messages = { - /** - * A fake, hard-coded Discord command to spoof the presence of - * official Discord client (which makes browser to actually start a - * communication with the ArmCord). - */ - handShake: { - /** Message command. */ - cmd: "DISPATCH", - /** Message data. */ - data: { - /** Message scheme version. */ - v: 1, - /** Client properties. */ - config: { - /** Discord CDN host (hard-coded for `dicscord.com` instance). */ - cdn_host: "cdn.discordapp.com", - /** API endpoint (hard-coded for `dicscord.com` instance). */ - api_endpoint: "//discord.com/api", - /** Client type. Can be (probably) `production` or `canary`. */ - environment: "production" - } - }, - evt: "READY", - nonce: null - } -}; - -/** - * Tries to reserve the server at given port. - * - * @returns `Promise`, which always resolves (either to `Server` on - * success or `null` on failure). - */ -async function getServer(port: number) { - const {WebSocketServer} = await import("ws"); - return new Promise | null>((resolve) => { - const wss = new WebSocketServer({host: "127.0.0.1", port}); - wss.once("listening", () => resolve(wss)); - wss.once("error", () => resolve(null)); - }); -} - -/** - * Tries to start a WebSocket server at given port range. If it suceed, it will - * listen to the browser requests which are meant to be sent to official - * Discord client. - * - * Currently it supports only the invitation link requests. - * - */ -export default async function startServer() { - function isJsonSyntaxCorrect(string: string) { - try { - JSON.parse(string); - } catch { - return false; - } - return true; - } - /** Known Discord instances, including the official ones. */ - const knownInstancesList = [ - ["Discord", new URL("https://discord.com/app")], - ["Discord Canary", new URL("https://canary.discord.com/app")], - ["Discord PTB", new URL("https://ptb.discord.com/app")], - ["Fosscord", new URL("https://dev.fosscord.com/app")] - ] as const; - - let wss = null, - wsPort = 6463; - for (const port of range(6463, 6472)) { - wss = await getServer(port); - if (wss !== null) { - void wsLog("ArmCord is listening at " + port.toString()); - wsPort = port; - break; - } - } - if (wss === null) return; - let lock = false; - wss.on("connection", (wss, request) => { - const origin = request.headers.origin ?? "https://discord.com"; - let known = false; - for (const instance of knownInstancesList) { - if (instance[1].origin === origin) known = true; - } - if (!known) return; - wss.send(JSON.stringify(messages.handShake)); - wss.once("message", (data, isBinary) => { - if (lock) return; - lock = true; - let parsedData: unknown = data; - if (!isBinary) parsedData = data.toString(); - if (isJsonSyntaxCorrect(parsedData as string)) parsedData = JSON.parse(parsedData as string); - if (isInviteResponse(parsedData)) { - // Replies to browser, so it finds the communication successful. - wss.send( - JSON.stringify({ - cmd: parsedData.cmd, - data: { - invite: null, - code: parsedData.args.code - }, - evt: null, - nonce: parsedData.nonce - }) - ); - createInviteWindow(); - const child = inviteWindow; - if (child === undefined) return; - void child.loadURL(origin + "/invite/" + parsedData.args.code); - child.webContents.once("did-finish-load", () => { - child.show(); - }); - child.webContents.once("will-navigate", () => { - lock = false; - child.close(); - }); - child.on("close", (e) => { - lock = false; - }); - // Blocks requests to ArmCord's WS, to prevent loops. - child.webContents.session.webRequest.onBeforeRequest( - { - urls: ["ws://127.0.0.1:" + wsPort.toString() + "/*"] - }, - (_details, callback) => callback({cancel: true}) - ); - } - }); - }); -} diff --git a/src/tray.ts b/src/tray.ts index 22fa797..464697c 100644 --- a/src/tray.ts +++ b/src/tray.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import {app, Menu, Tray, nativeImage} from "electron"; -import {mainWindow} from "./window"; +import {createInviteWindow, mainWindow} from "./window"; import {getConfig, getConfigLocation, setWindowState, getDisplayVersion} from "./utils"; import * as path from "path"; import {createSettingsWindow} from "./settings/main"; @@ -113,8 +113,7 @@ app.whenReady().then(async () => { { label: "Support Discord Server", click: function () { - mainWindow.show(); - mainWindow.loadURL("https://discord.gg/TnhxcqynZ2"); + createInviteWindow("TnhxcqynZ2"); } }, { diff --git a/src/window.ts b/src/window.ts index 13056ab..372c8ad 100644 --- a/src/window.ts +++ b/src/window.ts @@ -17,7 +17,6 @@ import { import {registerIpc} from "./ipc"; import {setMenu} from "./menu"; import * as fs from "fs"; -import startServer from "./socket"; import contextMenu from "electron-context-menu"; import os from "os"; import {tray} from "./tray"; @@ -292,7 +291,7 @@ export function createTransparentWindow() { }); doAfterDefiningTheWindow(); } -export function createInviteWindow() { +export function createInviteWindow(code: string) { inviteWindow = new BrowserWindow({ width: 800, height: 600, @@ -306,5 +305,16 @@ export function createInviteWindow() { spellcheck: true } }); - inviteWindow.hide(); + var formInviteURL = `https://discord.com/invite/${code}`; + inviteWindow.webContents.session.webRequest.onBeforeRequest((details, callback) => { + if (details.url.includes("ws://")) return callback({cancel: true}); + return callback({}); + }); + inviteWindow.loadURL(formInviteURL); + inviteWindow.webContents.once("did-finish-load", () => { + inviteWindow.show(); + inviteWindow.webContents.once("will-navigate", () => { + inviteWindow.close(); + }); + }); }