Skip to content

Commit

Permalink
feat: .native.js support
Browse files Browse the repository at this point in the history
  • Loading branch information
twnlink committed Jun 25, 2024
1 parent d696c51 commit bf0e0b8
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 39 deletions.
110 changes: 93 additions & 17 deletions injector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ const getNeptuneBundle = () =>
? fetchPromise
: Promise.resolve(
fs.readFileSync(path.join(localBundle, "neptune.js"), "utf8") +
`\n//# sourceMappingURL=file:////${path.join(localBundle, "neptune.js.map")}`,
`\n//# sourceMappingURL=file:////${path.join(
localBundle,
"neptune.js.map"
)}`
);
// #endregion

Expand All @@ -64,7 +67,7 @@ electron.ipcMain.handle("NEPTUNE_BUNDLE_FETCH", getNeptuneBundle);
// #region Redux Devtools
electron.app.whenReady().then(() => {
electron.session.defaultSession.loadExtension(
path.join(process.resourcesPath, "app", "redux-devtools"),
path.join(process.resourcesPath, "app", "redux-devtools")
);
});
// #endregion
Expand All @@ -81,13 +84,13 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") {
{
requestId: params.requestId,
},
sessionId,
sessionId
);

let body = res.base64Encoded ? atob(res.body) : res.body;
body = body.replace(
/<meta http-equiv="Content-Security-Policy" content=".*?">/,
"<!-- neptune removed csp -->",
"<!-- neptune removed csp -->"
);

// Add header to identify patched request in cache
Expand All @@ -104,7 +107,7 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") {
responseHeaders: params.responseHeaders,
body: btoa(body),
},
sessionId,
sessionId
);
} else if (method === "Target.attachedToTarget") {
const { sessionId } = params;
Expand All @@ -123,7 +126,7 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") {
},
],
},
sessionId,
sessionId
);
}
});
Expand Down Expand Up @@ -152,10 +155,13 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") {
if (caches.length !== 1) return;
const { cacheId } = caches[0];

const { cacheDataEntries, returnCount } = await dbg.sendCommand("CacheStorage.requestEntries", {
cacheId,
pathFilter: "/index.html",
});
const { cacheDataEntries, returnCount } = await dbg.sendCommand(
"CacheStorage.requestEntries",
{
cacheId,
pathFilter: "/index.html",
}
);
if (returnCount !== 1) return;
const entry = cacheDataEntries[0];

Expand All @@ -169,14 +175,72 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") {
}
// #endregion

// #region IPC Bullshit
let evalHandleCount = 0;
let evalHandles = {};
electron.ipcMain.on("NEPTUNE_CREATE_EVAL_SCOPE", (ev, code) => {
const scopeEval = eval(`(function () {
try {
${code}
return (code) => eval(code)
} catch {}
return eval;
})()`);

const id = evalHandleCount++;
evalHandles[id] = scopeEval;

ev.returnValue = id;
});

electron.ipcMain.on("NEPTUNE_RUN_IN_EVAL_SCOPE", (ev, scopeId, code) => {
try {
const retVal = evalHandles[scopeId](code);

if (retVal?.then && retVal?.catch) {
const promiseId = "NEPTUNE_PROMISE_" + Math.random().toString().slice(2);
ev.returnValue = { type: "promise", value: promiseId };

try {
const getAllWindows = () => electron.BrowserWindow.getAllWindows();

retVal.then((v) =>
getAllWindows().forEach((w) =>
w.webContents.send(promiseId, { type: "resolve", value: v })
)
);

retVal.catch((v) =>
getAllWindows().forEach((w) =>
w.webContents.send(promiseId, { type: "reject", value: v })
)
);
} catch {}
}

ev.returnValue = { type: "success", value: retVal };
} catch (err) {
ev.returnValue = { type: "error", value: err };
}
});

electron.ipcMain.on("NEPTUNE_DELETE_EVAL_SCOPE", (ev, arg) => {
delete evalHandles[arg];
ev.returnValue = true;
});
// #endregion

// #region BrowserWindow
const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, {
construct(target, args) {
const options = args[0];
let originalPreload;

// tidal-hifi does not set the title, rely on dev tools instead.
const isTidalWindow = options.title == "TIDAL" || options.webPreferences?.devTools;
const isTidalWindow =
options.title == "TIDAL" || options.webPreferences?.devTools;

if (isTidalWindow) {
originalPreload = options.webPreferences?.preload;
Expand All @@ -185,8 +249,9 @@ const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, {
options.webPreferences.preload = path.join(__dirname, "preload.js");

// Shhh. I can feel your judgement from here. It's okay. Let it out. Everything will be alright in the end.
options.webPreferences.contextIsolation = false;
options.webPreferences.nodeIntegration = true;
// options.webPreferences.contextIsolation = false;
// options.webPreferences.nodeIntegration = true;
options.webPreferences.sandbox = false;

// Allows local plugin loading
options.webPreferences.allowDisplayingInsecureContent = true;
Expand All @@ -198,9 +263,16 @@ const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, {

window.webContents.originalPreload = originalPreload;

window.webContents.on("did-navigate", () => {
// Clean up eval handles
evalHandles = {}
})

attachDebugger(
window.webContents.debugger,
options.webPreferences?.devTools ? "listen.tidal.com" : "desktop.tidal.com", // tidal-hifi uses listen.tidal.com
options.webPreferences?.devTools
? "listen.tidal.com"
: "desktop.tidal.com" // tidal-hifi uses listen.tidal.com
);
return window;
},
Expand All @@ -226,12 +298,16 @@ electron.Menu.buildFromTemplate = (template) => {
};
// #endregion

logger.log("Starting original...");
// #region Start original
logger.log("Starting original...");

let originalPath = path.join(process.resourcesPath, "app.asar");
if (!fs.existsSync(originalPath)) originalPath = path.join(process.resourcesPath, "original.asar");
if (!fs.existsSync(originalPath))
originalPath = path.join(process.resourcesPath, "original.asar");

const originalPackage = require(path.resolve(path.join(originalPath, "package.json")));
const originalPackage = require(path.resolve(
path.join(originalPath, "package.json")
));
const startPath = path.join(originalPath, originalPackage.main);

require.main.filename = startPath;
Expand Down
93 changes: 78 additions & 15 deletions injector/preload.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,84 @@
const electron = require("electron");

const electronPath = require.resolve("electron");
delete require.cache[electronPath].exports;

// God, have mercy on my soul. I'm so sorry.
require.cache[electronPath].exports = {
...electron,
contextBridge: {
exposeInMainWorld(name, properties) {
window[name] = properties;
},
},
};

electron.ipcRenderer.invoke("NEPTUNE_BUNDLE_FETCH").then((bundle) => {
electron.webFrame.executeJavaScript(bundle);
});

const originalPreload = electron.ipcRenderer.sendSync("NEPTUNE_ORIGINAL_PRELOAD");
if (originalPreload) require(originalPreload);
function createEvalScope(code) {
return electron.ipcRenderer.sendSync("NEPTUNE_CREATE_EVAL_SCOPE", code);
}

function getNativeValue(id, name) {
if (
electron.ipcRenderer.sendSync(
"NEPTUNE_RUN_IN_EVAL_SCOPE",
id,
`typeof neptuneExports.${name}`
).value == "function"
)
return (...args) => {
funcReturn = electron.ipcRenderer.sendSync(
"NEPTUNE_RUN_IN_EVAL_SCOPE",
id,
`neptuneExports.${name}(${args
.map((arg) =>
typeof arg != "function" ? JSON.stringify(arg) : arg.toString()
)
.join(",")})`
);

if (funcReturn.type == "promise") {
return new Promise((res, rej) => {
electron.ipcRenderer.once(funcReturn.value, (ev, { type, value }) => {
type == "resolve" ? res(value) : rej(value);
});
});
}

if (funcReturn.type == "error") {
throw new Error(funcReturn.value);
}

return funcReturn.value;
};

return electron.ipcRenderer.sendSync(
"NEPTUNE_RUN_IN_EVAL_SCOPE",
id,
`neptuneExports.${name}`
);
}

function deleteEvalScope(id) {
return electron.ipcRenderer.sendSync("NEPTUNE_DELETE_EVAL_SCOPE", id);
}

electron.contextBridge.exposeInMainWorld("NeptuneNative", {
createEvalScope,
getNativeValue,
deleteEvalScope,
});

electron.contextBridge.exposeInMainWorld("electron", {
ipcRenderer: Object.fromEntries(
[
"on",
"off",
"once",
"addListener",
"removeListener",
"removeAllListeners",
"send",
"invoke",
"sendSync",
"postMessage",
"sendToHost",
].map((n) => [n, electron.ipcRenderer[n]])
),
});

const originalPreload = electron.ipcRenderer.sendSync(
"NEPTUNE_ORIGINAL_PRELOAD"
);

if (originalPreload) require(originalPreload);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "rollup --config rollup.config.js",
"format": "prettier -wc ./src",
"watch": "rollup --config rollup.config.js -w",
"run": "npm run build && set NEPTUNE_DIST_PATH = \"./dist\" && %localappdata%/TIDAL/TIDAL.exe"
"run": "npm run build && set NEPTUNE_DIST_PATH=%cd%\\dist&& %LOCALAPPDATA%\\TIDAL\\TIDAL.exe"
},
"keywords": [],
"author": "",
Expand Down
16 changes: 10 additions & 6 deletions src/api/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ export const getPluginById = (id) => pluginStore.find((p) => p.id == id);
export async function disablePlugin(id) {
getPluginById(id).enabled = false;
const onUnload = enabled?.[id]?.onUnload;
const unloadables = enabled?.[id]?.unloadables;

delete enabled[id];

try {
await onUnload?.();
} catch (e) {
console.error("Failed to completely clean up neptune plugin!\n", e);
} finally {
await unloadables.forEach(u => u())
}
}

Expand Down Expand Up @@ -52,8 +49,8 @@ async function runPlugin(plugin) {
manifest: plugin.manifest,
storage: persistentStorage,
addUnloadable(callback) {
unloadables.push(callback)
}
unloadables.push(callback);
},
};

const { onUnload, Settings } = await quartz(plugin.code, {
Expand Down Expand Up @@ -86,7 +83,14 @@ async function runPlugin(plugin) {
],
});

enabled[plugin.id] = { onUnload: onUnload ?? (() => {}), unloadables };
enabled[plugin.id] = {
onUnload: () => {
onUnload?.();
for (const ul of unloadables) {
ul();
}
},
};
if (Settings) enabled[plugin.id].Settings = Settings;
} catch (e) {
await disablePlugin(plugin.id);
Expand Down

0 comments on commit bf0e0b8

Please sign in to comment.