Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignition Fix #659

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ async function buildPlugin({ watch, noInstall, production, noReload, addon }: Ar
esbuild.context(
overwrites({
...common,
format: "cjs",
entryPoints: [path.join(folderPath, manifest.plaintextPatches)],
outfile: `${distPath}/plaintextPatches.js`,
}),
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-plugins/intl-loader.mts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ export default {
getTranslationImport: (importPath) => `import("${importPath}")`,
debug: !production,
preGenerateBinds: false,
getPrelude: () => `import {waitForProps} from '@webpack';`,
getPrelude: () => `import {getByProps} from '@webpack';`,
}).getOutput();

return {
contents: transformedOutput.replace(
/require\('@discord\/intl'\);/,
"await waitForProps('createLoader','IntlManager');",
"getByProps('createLoader','IntlManager');",
),
loader: "js",
};
Expand Down
6 changes: 5 additions & 1 deletion scripts/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ const contexts = await Promise.all([
entryPoints: ["src/renderer/index.ts"],
platform: "browser",
target: `chrome${CHROME_VERSION}`,
format: "iife",
footer: {
js: "//# sourceURL=replugged://RepluggedRenderer/renderer.js",
css: "/*# sourceURL=replugged://RepluggedRenderer/renderer.css */",
},
outfile: `${distDir}/renderer.js`,
format: "esm",
loader: {
".png": "dataurl",
},
Expand Down
6 changes: 3 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ electron.protocol.registerSchemesAsPrivileged([
},
]);

async function loadReactDevTools(): Promise<void> {
const rdtSetting = await getSetting("dev.replugged.Settings", "reactDevTools", false);
function loadReactDevTools(): void {
const rdtSetting = getSetting<boolean>("dev.replugged.Settings", "reactDevTools", false);

if (rdtSetting) {
void electron.session.defaultSession.loadExtension(CONFIG_PATHS["react-devtools"]);
Expand Down Expand Up @@ -180,7 +180,7 @@ electron.app.once("ready", () => {
cb({ path: filePath });
});

void loadReactDevTools();
loadReactDevTools();
});

// This module is required this way at runtime.
Expand Down
6 changes: 6 additions & 0 deletions src/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import "./quick-css";
import "./react-devtools";
import "./settings";
import "./themes";
import { readFileSync } from "fs";
import { join } from "path";

ipcMain.on(RepluggedIpcChannels.GET_DISCORD_PRELOAD, (event) => {
event.returnValue = (event.sender as RepluggedWebContents).originalPreload;
});

ipcMain.on(RepluggedIpcChannels.GET_REPLUGGED_RENDERER, (event) => {
event.returnValue = readFileSync(join(__dirname, "./renderer.js"), "utf-8");
});
4 changes: 2 additions & 2 deletions src/main/ipc/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async function github(
}

async function store(id: string): Promise<CheckResultSuccess | CheckResultFailure> {
const apiUrl = await getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
const apiUrl = getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
const STORE_BASE_URL = `${apiUrl}/api/v1/store`;
const manifestUrl = `${STORE_BASE_URL}/${id}`;
const asarUrl = `${manifestUrl}.asar`;
Expand Down Expand Up @@ -191,7 +191,7 @@ ipcMain.handle(
if (type === "replugged") {
// Manually set Path and URL for security purposes
path = "replugged.asar";
const apiUrl = await getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
const apiUrl = getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
url = `${apiUrl}/api/v1/store/dev.replugged.Replugged.asar`;
}

Expand Down
82 changes: 46 additions & 36 deletions src/main/ipc/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ IPC events:
- REPLUGGED_UNINSTALL_PLUGIN: returns whether a plugin by the provided name was successfully uninstalled
*/

import { readFile, readdir, readlink, rm, stat } from "fs/promises";
import { rm } from "fs/promises";
import { extname, join, sep } from "path";
import { ipcMain, shell } from "electron";
import { RepluggedIpcChannels, type RepluggedPlugin } from "../../types";
import { plugin } from "../../types/addon";
import type { Dirent, Stats } from "fs";
import { type Dirent, type Stats, readFileSync, readdirSync, readlinkSync, statSync } from "fs";
import { CONFIG_PATHS } from "src/util.mjs";

const PLUGINS_DIR = CONFIG_PATHS.plugins;
Expand All @@ -18,15 +18,15 @@ export const isFileAPlugin = (f: Dirent | Stats, name: string): boolean => {
return f.isDirectory() || (f.isFile() && extname(name) === ".asar");
};

async function getPlugin(pluginName: string): Promise<RepluggedPlugin> {
function getPlugin(pluginName: string): RepluggedPlugin {
const manifestPath = join(PLUGINS_DIR, pluginName, "manifest.json");
if (!manifestPath.startsWith(`${PLUGINS_DIR}${sep}`)) {
// Ensure file changes are restricted to the base path
throw new Error("Invalid plugin name");
}

const manifest: unknown = JSON.parse(
await readFile(manifestPath, {
readFileSync(manifestPath, {
encoding: "utf-8",
}),
);
Expand All @@ -38,56 +38,66 @@ async function getPlugin(pluginName: string): Promise<RepluggedPlugin> {
};

const cssPath = data.manifest.renderer?.replace(/\.js$/, ".css");
const hasCSS =
cssPath &&
(await stat(join(PLUGINS_DIR, pluginName, cssPath))
.then(() => true)
.catch(() => false));
try {
const hasCSS = cssPath && statSync(join(PLUGINS_DIR, pluginName, cssPath));

if (hasCSS) data.hasCSS = true;
if (hasCSS) data.hasCSS = true;
} catch {
data.hasCSS = false;
}

return data;
}

ipcMain.handle(
RepluggedIpcChannels.GET_PLUGIN,
async (_, pluginName: string): Promise<RepluggedPlugin | undefined> => {
try {
return await getPlugin(pluginName);
} catch {}
},
);
ipcMain.on(RepluggedIpcChannels.GET_PLUGIN, (event, pluginName: string) => {
try {
event.returnValue = getPlugin(pluginName);
} catch {}
});

ipcMain.handle(RepluggedIpcChannels.LIST_PLUGINS, async (): Promise<RepluggedPlugin[]> => {
ipcMain.on(RepluggedIpcChannels.LIST_PLUGINS, (event) => {
const plugins = [];

const pluginDirs = (
await Promise.all(
(
await readdir(PLUGINS_DIR, {
withFileTypes: true,
})
).map(async (f) => {
if (isFileAPlugin(f, f.name)) return f;
if (f.isSymbolicLink()) {
const actualPath = await readlink(join(PLUGINS_DIR, f.name));
const actualFile = await stat(actualPath);
const pluginDirs = readdirSync(PLUGINS_DIR, {
withFileTypes: true,
})
.map((f) => {
if (isFileAPlugin(f, f.name)) return f;
if (f.isSymbolicLink()) {
try {
const actualPath = readlinkSync(join(PLUGINS_DIR, f.name));
const actualFile = statSync(actualPath);

if (isFileAPlugin(actualFile, actualPath)) return f;
}
}),
)
).filter(Boolean) as Dirent[];
} catch {}
}

return void 0;
})
.filter(Boolean) as Dirent[];

for (const pluginDir of pluginDirs) {
try {
plugins.push(await getPlugin(pluginDir.name));
plugins.push(getPlugin(pluginDir.name));
} catch (e) {
console.error(`Invalid plugin: ${pluginDir.name}`);
console.error(e);
}
}
event.returnValue = plugins;
});

ipcMain.on(RepluggedIpcChannels.READ_PLUGIN_PLAINTEXT_PATCHES, (event, pluginName) => {
const plugin = getPlugin(pluginName);
if (!plugin.manifest.plaintextPatches) return;

const path = join(CONFIG_PATHS.plugins, pluginName, plugin.manifest.plaintextPatches);
if (!path.startsWith(`${PLUGINS_DIR}${sep}`)) {
// Ensure file changes are restricted to the base path
throw new Error("Invalid plugin name");
}

return plugins;
if (path) event.returnValue = readFileSync(path, "utf-8");
});

ipcMain.handle(RepluggedIpcChannels.UNINSTALL_PLUGIN, async (_, pluginName: string) => {
Expand Down
2 changes: 1 addition & 1 deletion src/main/ipc/react-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const OUTPUT_PATH = join(CONFIG_PATHS["react-devtools"]);
const ZIP_PATH = join(OUTPUT_PATH, "extension.zip");

ipcMain.handle(RepluggedIpcChannels.DOWNLOAD_REACT_DEVTOOLS, async () => {
const apiUrl = await getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
const apiUrl = getSetting("dev.replugged.Settings", "apiUrl", WEBSITE_URL);
const REACT_DEVTOOLS_URL = `${apiUrl}/api/v1/react-devtools`;

let buffer;
Expand Down
95 changes: 46 additions & 49 deletions src/main/ipc/settings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { readFile, writeFile } from "fs/promises";
import { resolve, sep } from "path";
import { ipcMain, shell } from "electron";
import { RepluggedIpcChannels } from "../../types";
Expand All @@ -8,6 +7,7 @@ import type {
TransactionHandler,
} from "../../types/settings";
import { CONFIG_PATHS } from "src/util.mjs";
import { readFileSync, writeFileSync } from "fs";

const SETTINGS_DIR = CONFIG_PATHS.settings;

Expand All @@ -20,53 +20,49 @@ export function getSettingsPath(namespace: string): string {
return resolved;
}

async function readSettings(namespace: string): Promise<Map<string, unknown>> {
function readSettings(namespace: string): Map<string, unknown> {
const path = getSettingsPath(namespace);
try {
const data = await readFile(path, "utf8");
const data = readFileSync(path, "utf8");
return new Map(Object.entries(JSON.parse(data)));
} catch {
return new Map();
}
}

function writeSettings(namespace: string, settings: SettingsMap): Promise<void> {
return writeFile(
function writeSettings(namespace: string, settings: SettingsMap): void {
writeFileSync(
getSettingsPath(namespace),
JSON.stringify(Object.fromEntries(settings.entries()), null, 2),
"utf8",
);
}

const locks: Record<string, Promise<unknown> | undefined> = {};
const locks: Record<string, (() => unknown) | undefined> = {};

async function transaction<T>(namespace: string, handler: TransactionHandler<T>): Promise<T> {
const lock = locks[namespace] ?? Promise.resolve();
function transaction<T>(namespace: string, handler: TransactionHandler<T>): T {
const lock = locks[namespace];

const result = lock.then(() => handler());
if (lock) lock();

locks[namespace] = result.catch(() => {});
const result = handler();

locks[namespace] = () => result;
return result;
}

export async function readTransaction<T>(
namespace: string,
handler: SettingsTransactionHandler<T>,
): Promise<T> {
return transaction(namespace, async () => {
const settings = await readSettings(namespace);
export function readTransaction<T>(namespace: string, handler: SettingsTransactionHandler<T>): T {
return transaction(namespace, () => {
const settings = readSettings(namespace);
return handler(settings);
});
}

export async function writeTransaction<T>(
namespace: string,
handler: SettingsTransactionHandler<T>,
): Promise<T> {
return transaction(namespace, async () => {
const postHandlerTransform: Array<(settings: SettingsMap) => void | Promise<void>> = [];
export function writeTransaction<T>(namespace: string, handler: SettingsTransactionHandler<T>): T {
return transaction(namespace, () => {
const postHandlerTransform: Array<(settings: SettingsMap) => void | void> = [];

const settings = await readSettings(namespace);
const settings = readSettings(namespace);
if (namespace.toLowerCase() === "dev.replugged.settings") {
// Prevent the "apiUrl" setting from changing
const originalValue = settings.get("apiUrl");
Expand All @@ -79,52 +75,53 @@ export async function writeTransaction<T>(
});
}

const res = await handler(settings);
const res = handler(settings);

for (const transform of postHandlerTransform) {
await transform(settings);
transform(settings);
}

await writeSettings(namespace, settings);
writeSettings(namespace, settings);
return res;
});
}

export async function getSetting<T>(namespace: string, key: string, fallback: T): Promise<T>;
export async function getSetting<T>(
namespace: string,
key: string,
fallback?: T,
): Promise<T | undefined>;
export async function getSetting<T>(
namespace: string,
key: string,
fallback?: T,
): Promise<T | undefined> {
const setting = (await readTransaction(namespace, (settings) => settings.get(key))) as T;
export function getSetting<T>(namespace: string, key: string, fallback: T): T;
export function getSetting<T>(namespace: string, key: string, fallback?: T): T | undefined;
export function getSetting<T>(namespace: string, key: string, fallback?: T): T | undefined {
const setting = readTransaction(namespace, (settings) => settings.get(key)) as T;
return setting ?? fallback;
}

ipcMain.handle(RepluggedIpcChannels.GET_SETTING, async (_, namespace: string, key: string) =>
getSetting(namespace, key),
ipcMain.on(
RepluggedIpcChannels.GET_SETTING,
(event, namespace: string, key: string) => (event.returnValue = getSetting(namespace, key)),
);

ipcMain.handle(RepluggedIpcChannels.HAS_SETTING, async (_, namespace: string, key: string) =>
readTransaction(namespace, (settings) => settings.has(key)),
ipcMain.on(
RepluggedIpcChannels.HAS_SETTING,
(event, namespace: string, key: string) =>
(event.returnValue = readTransaction(namespace, (settings) => settings.has(key))),
);

ipcMain.handle(
ipcMain.on(
RepluggedIpcChannels.SET_SETTING,
(_, namespace: string, key: string, value: unknown) =>
void writeTransaction(namespace, (settings) => settings.set(key, value)),
(event, namespace: string, key: string, value: unknown) =>
(event.returnValue = writeTransaction(namespace, (settings) => settings.set(key, value))),
);

ipcMain.handle(RepluggedIpcChannels.DELETE_SETTING, (_, namespace: string, key: string) =>
writeTransaction(namespace, (settings) => settings.delete(key)),
ipcMain.on(
RepluggedIpcChannels.DELETE_SETTING,
(event, namespace: string, key: string) =>
(event.returnValue = writeTransaction(namespace, (settings) => settings.delete(key))),
);

ipcMain.handle(RepluggedIpcChannels.GET_ALL_SETTINGS, async (_, namespace: string) =>
readTransaction(namespace, (settings) => Object.fromEntries(settings.entries())),
ipcMain.on(
RepluggedIpcChannels.GET_ALL_SETTINGS,
(event, namespace: string) =>
(event.returnValue = readTransaction(namespace, (settings) =>
Object.fromEntries(settings.entries()),
)),
);

ipcMain.on(RepluggedIpcChannels.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
Loading
Loading