diff --git a/scripts/build.mjs b/scripts/build.mjs index 53f6ba1..2d593da 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -46,6 +46,7 @@ const config = { ".png": "dataurl" }, define: { + window: "globalThis", __DEV__: dev ?? JSON.stringify(releaseBranch !== "main") }, inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"], diff --git a/src/entry.ts b/src/entry.ts index 73eabcf..e729c78 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,10 +1,10 @@ +import type { Metro } from "@metro/types"; import { version } from "bunny-build-info"; -const { instead } = require("spitroast"); -// @ts-ignore - shut up fr +// @ts-ignore - window is defined later in the bundle, so we assign it early globalThis.window = globalThis; -async function initializeBunny() { +async function initializeRevenge() { try { // Make 'freeze' and 'seal' do nothing Object.freeze = Object.seal = Object; @@ -18,51 +18,80 @@ async function initializeBunny() { console.log(stack ?? e?.toString?.() ?? e); alert([ "Failed to load Revenge!\n", - `Build Number: ${ClientInfoManager.Build}`, + `Build Number: ${ClientInfoManager.getConstants().Build}`, `Revenge: ${version}`, stack || e?.toString?.(), ].join("\n")); } } -// @ts-ignore -if (typeof globalThis.__r !== "undefined") { - initializeBunny(); -} else { - // We hold calls from the native side - function onceIndexRequired(originalRequire: any) { - const batchedBridge = window.__fbBatchedBridge; +if (typeof window.__r === "undefined") { + // Used for storing the current require function for the global.__r getter defined below + var _requireFunc: any; - const callQueue = new Array; - const unpatchHook = instead("callFunctionReturnFlushedQueue", batchedBridge, (args: any, orig: any) => { - if (args[0] === "AppRegistry" || !batchedBridge.getCallableModule(args[0])) { - callQueue.push(args); - return batchedBridge.flushedQueue(); + // Calls from the native side are deferred until the index.ts(x) is loaded + // Revenge delays the execution of index.ts(x) because Revenge's initialization is asynchronous + interface DeferredQueue { + object: any; + original: any; + method: string; + args: any[]; + } + + const deferredCalls: Array = []; + + const deferMethodExecution = (object: any, method: string, condition?: (...args: any[]) => boolean) => { + const originalMethod = object[method]; + object[method] = (...args: any[]) => { + if (condition && !condition(...args)) { + originalMethod(...args); + return; } - return orig.apply(batchedBridge, args); - }); + deferredCalls.push({ object, original: originalMethod, method, args }); + }; + } + + const resumeDeferredAndRestore = () => { + for (const { object, method, args, original } of deferredCalls) { + object[method] = original; + object[method](...args); + } + + deferredCalls.length = 0; + } + + const onceIndexRequired = (originalRequire: Metro.RequireFn) => { + // We hold calls from the native side + if (window.__fbBatchedBridge) { + deferMethodExecution(window.__fbBatchedBridge, "callFunctionReturnFlushedQueue", args => { + // If the call is to AppRegistry, we want to defer it because it is not yet registered (Revenge delays it) + // Same goes to the non-callable modules, which are not registered yet, so we ensure that only registered ones can get through + return args[0] !== "AppRegistry" && window.__fbBatchedBridge.getCallableModule(args[0]); + }); + } + + // Introduced since RN New Architecture + if (window.RN$AppRegistry) { + deferMethodExecution(window.RN$AppRegistry, "runApplication"); + } const startDiscord = async () => { - await initializeBunny(); - unpatchHook(); + await initializeRevenge(); originalRequire(0); - callQueue.forEach(arg => - batchedBridge.getCallableModule(arg[0]) - && batchedBridge.__callFunction(...arg)); + resumeDeferredAndRestore(); }; startDiscord(); } - var _requireFunc: any; // We can't set properties to 'this' during __r set for some reason - Object.defineProperties(globalThis, { __r: { configurable: true, get: () => _requireFunc, set(v) { + // _requireFunc is required here, because using 'this' here errors for some unknown reason _requireFunc = function patchedRequire(a: number) { // Initializing index.ts(x) if (a === 0) { @@ -85,4 +114,8 @@ if (typeof globalThis.__r !== "undefined") { set(v) { this.value = v; } } }); -} +} else { + // It is too late to late to hook __r, so we just initialize Revenge here + // Likely because of using the legacy loader (from Vendetta) + initializeRevenge(); +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index 7390be4..d8d38b4 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -6,6 +6,7 @@ declare global { var globalEvalWithSourceUrl: (script: string, sourceURL: string) => any; var nativePerformanceNow: typeof performance.now; var nativeModuleProxy: Record; + var __turboModuleProxy: (name: string) => any; interface Window { [key: string]: any; diff --git a/src/lib/api/debug.ts b/src/lib/api/debug.ts index 0bdd3f1..8d301c2 100644 --- a/src/lib/api/debug.ts +++ b/src/lib/api/debug.ts @@ -113,8 +113,8 @@ export function getDebugInfo() { } }, discord: { - version: NativeClientInfoModule.Version, - build: NativeClientInfoModule.Build, + version: NativeClientInfoModule.getConstants().Version, + build: NativeClientInfoModule.getConstants().Build, }, react: { version: React.version, diff --git a/src/lib/api/native/modules/index.ts b/src/lib/api/native/modules/index.ts index fdc2419..d6919cc 100644 --- a/src/lib/api/native/modules/index.ts +++ b/src/lib/api/native/modules/index.ts @@ -2,10 +2,34 @@ import { RNModules } from "./types"; const nmp = window.nativeModuleProxy; -export const NativeCacheModule = (nmp.NativeCacheModule ?? nmp.MMKVManager) as RNModules.MMKVManager; -export const NativeFileModule = (nmp.NativeFileModule ?? nmp.RTNFileManager ?? nmp.DCDFileManager) as RNModules.FileManager; -export const NativeClientInfoModule = nmp.NativeClientInfoModule ?? nmp.RTNClientInfoManager ?? nmp.InfoDictionaryManager; -export const NativeDeviceModule = nmp.NativeDeviceModule ?? nmp.RTNDeviceManager ?? nmp.DCDDeviceManager; -export const NativeThemeModule = nmp.NativeThemeModule ?? nmp.RTNThemeManager ?? nmp.DCDTheme; +function getNativeModule(...names: string[]): T | undefined { + for (const name of names) { + if (globalThis.__turboModuleProxy) { + const module = globalThis.__turboModuleProxy(name); + if (module) return module as T; + } -export const { BundleUpdaterManager } = nmp; + if (nmp[name]) return nmp[name] as T; + } + + return undefined; +} + +export const NativeCacheModule = getNativeModule( + "NativeCacheModule", "MMKVManager" +)!; +export const NativeFileModule = getNativeModule( + "NativeFileModule", "RTNFileManager", "DCDFileManager" +)!; +export const NativeClientInfoModule = getNativeModule( + "NativeClientInfoModule", "RTNClientInfoManager", "InfoDictionaryManager" +)!; +export const NativeDeviceModule = getNativeModule( + "NativeDeviceModule", "RTNDeviceManager", "DCDDeviceManager" +)!; +export const NativeThemeModule = getNativeModule( + "NativeThemeModule", "RTNThemeManager", "DCDTheme" +)!; +export const BundleUpdaterManager = getNativeModule( + "BundleUpdaterManager" +)!; diff --git a/src/lib/api/native/modules/types.ts b/src/lib/api/native/modules/types.ts index 4fd4501..ecbc283 100644 --- a/src/lib/api/native/modules/types.ts +++ b/src/lib/api/native/modules/types.ts @@ -86,4 +86,53 @@ export namespace RNModules { */ DocumentsDirPath: string; } + + export interface ClientInfoModule { + getConstants(): { + /** + * Sentry ingestion DSN URL for alpha/beta builds + */ + SentryAlphaBetaDsn: string + /** + * Sentry ingestion DSN URL for staff builds (?) + */ + SentryStaffDsn: string + /** + * Sentry ingestion DSN URL for stable builds + */ + SentryDsn: string + DeviceVendorID: string + Manifest: string + /** + * Version code + * + * Follows the format of `{MINOR}{CHANNEL}{PATCH}` for `{MINOR}.{PATCH} ({CHANNEL})` + * - `248200` for `248.0 (alpha)` + * - `247105` for `247.5 (beta)` + * - `246011` for `246.11 (stable)` + */ + Build: string + /** + * Version string + * + * Eg. `248.0` + */ + Version: string + /** + * Release channel + */ + ReleaseChannel: string + /** + * Matches `Version` + */ + OTABuild: string + /** + * Identifier for the installed client + * + * - **Android**: Package name + * - **iOS**: Bundle ID + */ + Identifier: string + } + } } diff --git a/src/metro/internals/caches.ts b/src/metro/internals/caches.ts index 34ed45c..b5192b8 100644 --- a/src/metro/internals/caches.ts +++ b/src/metro/internals/caches.ts @@ -18,7 +18,7 @@ export const getMetroCache = () => _metroCache; function buildInitCache() { const cache = { _v: CACHE_VERSION, - _buildNumber: NativeClientInfoModule.Build as number, + _buildNumber: NativeClientInfoModule.getConstants().Build, _modulesCount: Object.keys(window.modules).length, flagsIndex: {} as Record, findIndex: {} as Record, @@ -48,7 +48,7 @@ export async function initMetroCache() { _metroCache = null!; throw "cache invalidated; cache version outdated"; } - if (_metroCache._buildNumber !== NativeClientInfoModule.Build) { + if (_metroCache._buildNumber !== NativeClientInfoModule.getConstants().Build) { _metroCache = null!; throw "cache invalidated; version mismatch"; }