Compare commits

...

7 commits

Author SHA1 Message Date
smartfrigde
3854e5fa39 Add windows build 2022-04-19 20:19:00 +02:00
smartfrigde
d68cec6396 I forgor 2022-04-19 20:14:55 +02:00
smartfrigde
1f01ae67dd Add dev build CI 2022-04-19 20:13:46 +02:00
smartfrigde
7a7dd05163 Make invite websocket togglable 2022-04-19 19:59:52 +02:00
smartfrigde
37966b985a Fix websocket not unlocking when window is closed manually 2022-04-19 16:10:12 +02:00
smartfrigde
eda6621989 Fix websocket logger 2022-04-19 16:02:30 +02:00
smartfrigde
3d0fd7071b Add websocket for invitation link requests 2022-04-19 15:56:04 +02:00
9 changed files with 329 additions and 20 deletions

2
.github/release.md vendored Normal file
View file

@ -0,0 +1,2 @@
# Thanks for checking out ArmCord dev build!
Make sure to join our [Discord server](https://discord.gg/uaW5vMY3V6) to share opinions, or to chat with ArmCord developers!

59
.github/workflows/dev.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Dev build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: sudo apt install -y wine
- run: npm install
- run: npm run CIbuild
- id: vars
shell: bash
run: |
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.vars.outputs.sha_short }}
release_name: Release ${{ steps.vars.outputs.sha_short }}
draft: true
prerelease: true
body_path: ./.github/release.md
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0.zip
asset_name: ArmCordLinux.zip
asset_content_type: application/octet-stream
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0-win.zip
asset_name: ArmCordWindows.zip
asset_content_type: application/octet-stream
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: /home/runner/work/ArmCord/ArmCord/dist/ArmCord-3.1.0-mac.zip
asset_name: ArmCordWindows.zip
asset_content_type: application/octet-stream
- run: |
curl --request PATCH \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
https://api.github.com/repos/${{ github.repository }}/releases/${{steps.create_release.outputs.id}} \
-d '{"draft":false}'

59
package-lock.json generated
View file

@ -11,12 +11,13 @@
"license": "OSL-3.0", "license": "OSL-3.0",
"dependencies": { "dependencies": {
"electron-context-menu": "^3.1.2", "electron-context-menu": "^3.1.2",
"electron-tabs": "^0.17.0", "v8-compile-cache": "^2.3.0",
"v8-compile-cache": "^2.3.0" "ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/electron-json-storage": "^4.5.0", "@types/electron-json-storage": "^4.5.0",
"@types/node": "^17.0.24", "@types/node": "^17.0.24",
"@types/ws": "^8.5.3",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"electron": "^18.0.4", "electron": "^18.0.4",
"electron-builder": "^22.5.1", "electron-builder": "^22.5.1",
@ -121,6 +122,15 @@
"integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==",
"dev": true "dev": true
}, },
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "15.0.14", "version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
@ -1119,11 +1129,6 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/electron-tabs": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/electron-tabs/-/electron-tabs-0.17.0.tgz",
"integrity": "sha512-jFv6WOeumSR5q2Cf6WOghE7CTdxPB0mSuPw8dGwz1OAG8MJMQn/kd/ghmvRPwoOYK77v4d9YligqjXIQc2oPcg=="
},
"node_modules/electron/node_modules/@types/node": { "node_modules/electron/node_modules/@types/node": {
"version": "16.11.26", "version": "16.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz",
@ -2907,6 +2912,26 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
}, },
"node_modules/ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
"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
}
}
},
"node_modules/xdg-basedir": { "node_modules/xdg-basedir": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
@ -3057,6 +3082,15 @@
"integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==", "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==",
"dev": true "dev": true
}, },
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/yargs": { "@types/yargs": {
"version": "15.0.14", "version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
@ -3885,11 +3919,6 @@
} }
} }
}, },
"electron-tabs": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/electron-tabs/-/electron-tabs-0.17.0.tgz",
"integrity": "sha512-jFv6WOeumSR5q2Cf6WOghE7CTdxPB0mSuPw8dGwz1OAG8MJMQn/kd/ghmvRPwoOYK77v4d9YligqjXIQc2oPcg=="
},
"emoji-regex": { "emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -5285,6 +5314,12 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
}, },
"ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
"requires": {}
},
"xdg-basedir": { "xdg-basedir": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",

View file

@ -9,6 +9,7 @@
"start": "npm run build && electron ./ts-out/main.js", "start": "npm run build && electron ./ts-out/main.js",
"package": "npm run build && electron-builder", "package": "npm run build && electron-builder",
"format": "prettier --write src/**/*", "format": "prettier --write src/**/*",
"CIbuild": "npm run build && electron-builder --linux zip && electron-builder --windows zip && electron-builder --macos zip",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"repository": { "repository": {
@ -24,6 +25,7 @@
"devDependencies": { "devDependencies": {
"@types/electron-json-storage": "^4.5.0", "@types/electron-json-storage": "^4.5.0",
"@types/node": "^17.0.24", "@types/node": "^17.0.24",
"@types/ws": "^8.5.3",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"electron": "^18.0.4", "electron": "^18.0.4",
"electron-builder": "^22.5.1", "electron-builder": "^22.5.1",
@ -33,8 +35,8 @@
}, },
"dependencies": { "dependencies": {
"electron-context-menu": "^3.1.2", "electron-context-menu": "^3.1.2",
"electron-tabs": "^0.17.0", "v8-compile-cache": "^2.3.0",
"v8-compile-cache": "^2.3.0" "ws": "^8.5.0"
}, },
"build": { "build": {
"appId": "com.smartfridge.armcord", "appId": "com.smartfridge.armcord",

View file

@ -46,7 +46,9 @@
minimizeToTray: true, minimizeToTray: true,
automaticPatches: false, automaticPatches: false,
mods: "cumcord", mods: "cumcord",
blurType: "acrylic" blurType: "acrylic",
inviteWebsocket: true,
doneSetup: true
}); });
fade(document.getElementById("setup")); fade(document.getElementById("setup"));
setTimeout(function () { setTimeout(function () {
@ -98,7 +100,9 @@
minimizeToTray: true, minimizeToTray: true,
automaticPatches: false, automaticPatches: false,
mods: mod, mods: mod,
blurType: "acrylic" blurType: "acrylic",
inviteWebsocket: true,
doneSetup: true
}); });
fade(document.getElementById("setup")); fade(document.getElementById("setup"));
setTimeout(function () { setTimeout(function () {
@ -113,7 +117,9 @@
minimizeToTray: true, minimizeToTray: true,
automaticPatches: false, automaticPatches: false,
mods: "none", mods: "none",
blurType: "acrylic" blurType: "acrylic",
inviteWebsocket: true,
doneSetup: true
}); });
fade(document.getElementById("setup")); fade(document.getElementById("setup"));
setTimeout(function () { setTimeout(function () {

View file

@ -74,7 +74,9 @@
minimizeToTray: document.getElementById("tray").checked, minimizeToTray: document.getElementById("tray").checked,
automaticPatches: document.getElementById("patches").checked, automaticPatches: document.getElementById("patches").checked,
mods: document.getElementById("mod").value, mods: document.getElementById("mod").value,
blurType: "acrylic" blurType: "acrylic",
inviteWebsocket: true,
doneSetup: true
}); });
}); });
</script> </script>

182
src/socket.ts Normal file
View file

@ -0,0 +1,182 @@
// 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.
import type {Server, WebSocket} from "ws";
import {inviteWindow, createInviteWindow} from "./window";
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<InviteResponse>)?.cmd !== "INVITE_BROWSER") return false;
if (typeof (data as Partial<InviteResponse>)?.args?.code !== "string") return false;
if (typeof (data as Partial<InviteResponse>)?.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<WebSocket>` on
* success or `null` on failure).
*/
async function getServer(port: number) {
const {WebSocketServer} = await import("ws");
return new Promise<Server<WebSocket> | 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}));
}
})
})
}

View file

@ -41,6 +41,7 @@ export function setup() {
automaticPatches: false, automaticPatches: false,
mods: "cumcord", mods: "cumcord",
blurType: "acrylic", blurType: "acrylic",
inviteWebsocket: true,
doneSetup: false doneSetup: false
}; };
setConfigBulk({ setConfigBulk({
@ -72,6 +73,7 @@ export interface Settings {
automaticPatches: boolean; automaticPatches: boolean;
mods: string; mods: string;
blurType: string; blurType: string;
inviteWebsocket: boolean;
doneSetup: boolean; doneSetup: boolean;
} }
export async function getConfig(object: string) { export async function getConfig(object: string) {

View file

@ -6,9 +6,10 @@ import {BrowserWindow, shell, app, ipcMain, dialog} from "electron";
import path from "path"; import path from "path";
import {checkIfConfigIsBroken, firstRun, getConfig, contentPath} from "./utils"; import {checkIfConfigIsBroken, firstRun, getConfig, contentPath} from "./utils";
import {registerIpc} from "./ipc"; import {registerIpc} from "./ipc";
import startServer from "./socket"
import contextMenu from "electron-context-menu"; import contextMenu from "electron-context-menu";
export let mainWindow: BrowserWindow; export let mainWindow: BrowserWindow;
export let inviteWindow: BrowserWindow;
let guestWindows: BrowserWindow[] = []; let guestWindows: BrowserWindow[] = [];
contextMenu({ contextMenu({
showSaveImageAs: true, showSaveImageAs: true,
@ -16,7 +17,7 @@ contextMenu({
showSearchWithGoogle: true showSearchWithGoogle: true
}); });
function doAfterDefiningTheWindow() { async function doAfterDefiningTheWindow() {
checkIfConfigIsBroken(); checkIfConfigIsBroken();
registerIpc(); registerIpc();
mainWindow.webContents.userAgent = mainWindow.webContents.userAgent =
@ -39,6 +40,9 @@ function doAfterDefiningTheWindow() {
} }
}); });
console.log(contentPath); console.log(contentPath);
if (await getConfig("inviteWebsocket") == true) {
startServer()
}
try { try {
mainWindow.loadFile(contentPath); mainWindow.loadFile(contentPath);
} catch (e) { } catch (e) {
@ -165,3 +169,18 @@ export function createTabsGuest(number: number) {
guestWindows[number].loadURL("https://discord.com/app"); guestWindows[number].loadURL("https://discord.com/app");
} }
} }
export function createInviteWindow() {
inviteWindow = new BrowserWindow({
width: 800,
height: 600,
title: "ArmCord Invite Manager",
darkTheme: true,
icon: path.join(__dirname, "/assets/icon_transparent.png"),
frame: true,
autoHideMenuBar: true,
webPreferences: {
spellcheck: true
}
});
inviteWindow.hide()
}