diff --git a/lib/assets/src/preinit.ts b/lib/assets/src/preinit.ts index eda3ab7..231862d 100644 --- a/lib/assets/src/preinit.ts +++ b/lib/assets/src/preinit.ts @@ -12,7 +12,7 @@ export let AssetRegistry: ReactNative.AssetsRegistry export let AssetRegistryModuleId: number // Tracking/caching assets -const unsubAR = waitForModules(byProps('registerAsset'), (id, exports) => { +const unsubAR = waitForModules(byProps('registerAsset'), (exports, id) => { AssetRegistryModuleId = id AssetRegistry = exports as ReactNative.AssetsRegistry @@ -64,7 +64,7 @@ const unsubRAS = waitForModules( byName<{ addCustomSourceTransformer: (transformer: (arg: { asset: Asset }) => ReactNativeAsset) => void }>('resolveAssetSource'), - (_, rAS) => { + rAS => { unsubRAS() // Custom assets diff --git a/lib/discord/src/actions.ts b/lib/discord/src/actions.ts index 6da5e3d..20b1339 100644 --- a/lib/discord/src/actions.ts +++ b/lib/discord/src/actions.ts @@ -18,7 +18,7 @@ import type { DiscordModules } from './types' export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCreators = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('hideActionSheet'), byDependencies([ @@ -45,7 +45,7 @@ export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCr export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('openAlert', 'dismissAlert'), byDependencies([ @@ -81,5 +81,5 @@ export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators = pro }, ) - for (const module of generator) if (module.open.length === 1) return (ToastActionCreators = module) + for (const [module] of generator) if (module.open.length === 1) return (ToastActionCreators = module) })! diff --git a/lib/discord/src/common/flux.ts b/lib/discord/src/common/flux.ts index 460031f..b970912 100644 --- a/lib/discord/src/common/flux.ts +++ b/lib/discord/src/common/flux.ts @@ -1,12 +1,13 @@ import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters' -import { lookupModuleId } from '@revenge-mod/modules/finders/lookup' +import { lookupModule } from '@revenge-mod/modules/finders/lookup' import { waitForModules } from '@revenge-mod/modules/finders/wait' import type { DiscordModules } from '../types' +import type { Metro } from '@revenge-mod/modules/types' // ../discord_common/js/packages/flux -export const DispatcherModuleId = lookupModuleId( +export const [Dispatcher, DispatcherModuleId] = lookupModule( preferExports( byProps('_interceptors'), byDependencies([relativeDep(1), undefined, undefined, undefined, undefined, 2]), @@ -14,11 +15,9 @@ export const DispatcherModuleId = lookupModuleId( { includeUninitialized: true, }, -) +) as [DiscordModules.Flux.Dispatcher, Metro.ModuleID] -export const Dispatcher = __r(DispatcherModuleId!) as DiscordModules.Flux.Dispatcher - -waitForModules(byProps('_dispatchToken'), (_, store) => { +waitForModules(byProps('_dispatchToken'), store => { const name = store.getName() _stores[name] = store }) @@ -27,5 +26,5 @@ const _stores: Record = {} export const Stores = new Proxy(_stores, { ownKeys: () => Reflect.ownKeys(_stores), - get: (_, name) => Stores[name as string], + get: (_, name) => _stores[name as string], }) diff --git a/lib/discord/src/common/index.ts b/lib/discord/src/common/index.ts index 02951a6..43bc1c8 100644 --- a/lib/discord/src/common/index.ts +++ b/lib/discord/src/common/index.ts @@ -1,15 +1,15 @@ import { byName, byProps } from '@revenge-mod/modules/finders/filters' -import { lookupModule, lookupModuleId } from '@revenge-mod/modules/finders/lookup' +import { lookupModule } from '@revenge-mod/modules/finders/lookup' import type { DiscordModules } from '../types' +import type { Metro } from '@revenge-mod/modules/types' export { AppStartPerformance } from '../preinit' export * as Flux from './flux' // ../discord_common/js/shared/Logger.tsx -export const Logger = lookupModule(byName('Logger'))! +export const [Logger] = lookupModule(byName('Logger')) as [DiscordModules.Logger, Metro.ModuleID] // ../discord_common/js/packages/tokens/native.tsx -export const TokensModuleId = lookupModuleId(byProps('RawColor'))! -export const Tokens = __r(TokensModuleId) +export const [Tokens, TokensModuleId] = lookupModule(byProps('RawColor')) as [any, Metro.ModuleID] diff --git a/lib/discord/src/design.ts b/lib/discord/src/design.ts index a941ca1..175fa18 100644 --- a/lib/discord/src/design.ts +++ b/lib/discord/src/design.ts @@ -19,7 +19,7 @@ export let Design: Design = proxify( () => { // ID: 3236 // [3237, 1366, 3238, 3239, 2, ...]; - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('TableRow', 'Button'), byDependencies(looseDeps([relativeDep(1), undefined, relativeDep(2), relativeDep(3), 2])), @@ -43,7 +43,7 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => { // ID: 46 // Deps: [47, 48, 35, 49] - const module = lookupModule( + const [module] = lookupModule( preferExports( bySingleProp<{ FormSwitch: DiscordModules.Components.FormSwitch }>('FormSwitch'), byDependencies([ diff --git a/lib/discord/src/modules/settings/index.ts b/lib/discord/src/modules/settings/index.ts index 9f890a2..68f51c4 100644 --- a/lib/discord/src/modules/settings/index.ts +++ b/lib/discord/src/modules/settings/index.ts @@ -22,7 +22,7 @@ export function isSettingsModulesLoaded() { * @returns A function to unsubscribe from the event. * @throws Throws an error if the settings modules are already loaded. Check with {@link isSettingsModulesLoaded} first. */ -export function onceSettingsModulesLoaded(subcription: SettingsModulesLoadedSubscription) { +export function onSettingsModulesLoaded(subcription: SettingsModulesLoadedSubscription) { if (_data[2]) throw new Error('Settings modules already loaded') _subs.add(subcription) diff --git a/lib/discord/src/modules/settings/renderer.ts b/lib/discord/src/modules/settings/renderer.ts index 461ecb7..e379d69 100644 --- a/lib/discord/src/modules/settings/renderer.ts +++ b/lib/discord/src/modules/settings/renderer.ts @@ -11,7 +11,7 @@ export type SettingListRenderer = DiscordModules.Modules.Settings.SettingListRen export let SettingListRenderer: SettingListRenderer = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('SettingsList'), byDependencies( diff --git a/lib/discord/src/preinit.ts b/lib/discord/src/preinit.ts index b946bd8..047d591 100644 --- a/lib/discord/src/preinit.ts +++ b/lib/discord/src/preinit.ts @@ -1,10 +1,11 @@ import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters' import { lookupModule } from '@revenge-mod/modules/finders/lookup' +import type { Metro } from '@revenge-mod/modules/types' import type { DiscordModules } from './types' // ../discord_common/js/shared/AppStartPerformance.tsx -export const AppStartPerformance = lookupModule( +export const [AppStartPerformance] = lookupModule( preferExports( byProps('markAndLog'), byDependencies([relativeDep(-1), undefined, undefined, 2]), @@ -12,6 +13,6 @@ export const AppStartPerformance = lookupModule( { includeUninitialized: true, }, -)! +) as [DiscordModules.AppStartPerformance, Metro.ModuleID] -AppStartPerformance.mark('👊', 'Preinit') +AppStartPerformance.mark('👊', 'Pre-init') diff --git a/lib/discord/src/types/index.ts b/lib/discord/src/types/index.ts index 04254d4..a29284b 100644 --- a/lib/discord/src/types/index.ts +++ b/lib/discord/src/types/index.ts @@ -262,8 +262,8 @@ export namespace DiscordModules { onBlur?: () => void onFocus?: () => void - leadingIcon?: () => JSX.Element - trailingIcon?: () => JSX.Element + leadingIcon?: FC + trailingIcon?: FC leadingText?: string trailingText?: string description?: string diff --git a/lib/externals/src/browserify.ts b/lib/externals/src/browserify.ts index 99fd557..fa639e3 100644 --- a/lib/externals/src/browserify.ts +++ b/lib/externals/src/browserify.ts @@ -4,7 +4,7 @@ import { proxify } from '@revenge-mod/utils/proxy' export let nodeUtil: typeof import('node:util') = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports(byProps('inspect'), byDependencies([relativeDep(1), [], []])), { includeUninitialized: true, diff --git a/lib/externals/src/react-native-clipboard.ts b/lib/externals/src/react-native-clipboard.ts index b041868..fa8a1d1 100644 --- a/lib/externals/src/react-native-clipboard.ts +++ b/lib/externals/src/react-native-clipboard.ts @@ -16,7 +16,7 @@ let ClipboardModule: typeof import('@react-native-clipboard/clipboard') = proxif // r6 = 4792; // r5 = [27, 4793]; - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('useClipboard'), every( diff --git a/lib/externals/src/react-navigation.ts b/lib/externals/src/react-navigation.ts index 9151c53..737584f 100644 --- a/lib/externals/src/react-navigation.ts +++ b/lib/externals/src/react-navigation.ts @@ -1,13 +1,13 @@ import { byDependencies, byName, byProps, looseDeps, preferExports } from '@revenge-mod/modules/finders/filters' -import { lookupModule, lookupModuleId } from '@revenge-mod/modules/finders/lookup' +import { lookupModule } from '@revenge-mod/modules/finders/lookup' import { proxify } from '@revenge-mod/utils/proxy' -const _createClassModuleId = lookupModuleId(byName('_createClass')) -const _classCallCheckModuleId = lookupModuleId(byName('_classCallCheck')) +const [, _createClassModuleId] = lookupModule(byName('_createClass')) +const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck')) export let ReactNavigationNative: typeof import('@react-navigation/native') = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('useLinkTo'), byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])), @@ -26,7 +26,7 @@ export let ReactNavigationNative: typeof import('@react-navigation/native') = pr export let ReactNavigationStack: typeof import('@react-navigation/stack') = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('createStackNavigator'), byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])), diff --git a/lib/externals/src/shopify.ts b/lib/externals/src/shopify.ts index b5bda2d..ddc7523 100644 --- a/lib/externals/src/shopify.ts +++ b/lib/externals/src/shopify.ts @@ -6,7 +6,7 @@ import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@re export let FlashList: typeof import('@shopify/flash-list') = proxify( () => { - const module = lookupModule( + const [module] = lookupModule( preferExports( byProps('FlashList'), byDependencies([ diff --git a/lib/init/package.json b/lib/init/package.json deleted file mode 100644 index c957d57..0000000 --- a/lib/init/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@revenge-mod/init", - "private": true, - "type": "module", - "exports": { - ".": { - "default": "./src/index.ts" - } - } -} diff --git a/lib/init/src/index.ts b/lib/init/src/index.ts deleted file mode 100644 index 8cb170a..0000000 --- a/lib/init/src/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below. -import { patchMetroDefine } from '@revenge-mod/modules/_/metro' -import { interceptProperty } from '@revenge-mod/utils/objects' - -// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set. -// But both should be true either way. -const unintercept = interceptProperty('__d', (_, mDefine) => (unintercept(), patchMetroDefine(mDefine))) - -import { onModuleFirstRequired, onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions' -import { getErrorStack } from '@revenge-mod/utils/errors' - -onModuleFirstRequired(0, function onceIndexRequired() { - runCatching(() => { - if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections') - - // Initialize preinit libraries - require('@revenge-mod/modules/preinit') - require('@revenge-mod/react/preinit') - require('@revenge-mod/assets/preinit') - require('@revenge-mod/discord/preinit') - - onModuleInitialized(0, function onceIndexInitialized() { - runCatching(() => { - // Register all plugins - require('~/plugins/all') - - // Initialize init libraries - require('@revenge-mod/plugins/init') - }) - }) - }) -}) - -function runCatching(fn: () => void) { - try { - fn() - } catch (e) { - // TODO(init): Move to use native provided alert function, which will accept a string stack trace - // Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further - const { ClientInfoModule, DeviceModule } = require('@revenge-mod/discord/native') as - // biome-ignore format: Don't format this please - typeof import('@revenge-mod/discord/native') - - alert( - // biome-ignore lint/style/useTemplate: I want this to be readable, thank you - `Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` + - `Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})\n` + - `Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}\n\n` + - getErrorStack(e), - ) - } -} diff --git a/lib/modules/package.json b/lib/modules/package.json index 7f28340..dfefa96 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -6,6 +6,9 @@ "./finders/filters": { "default": "./src/finders/filters.ts" }, + "./finders/get": { + "default": "./src/finders/get.ts" + }, "./finders/lookup": { "default": "./src/finders/lookup.ts" }, diff --git a/lib/modules/src/finders/get.ts b/lib/modules/src/finders/get.ts new file mode 100644 index 0000000..30ad2da --- /dev/null +++ b/lib/modules/src/finders/get.ts @@ -0,0 +1,101 @@ +import { noopFalse } from '@revenge-mod/utils/callbacks' + +import { type LookupModulesOptions, lookupModule, lookupModuleByImportedPath } from './lookup' +import { type WaitForModulesOptions, waitForModuleByImportedPath, waitForModules } from './wait' + +import type { MaybeDefaultExportMatched } from '../types' +import type { Metro } from '../types' +import type { RunFilterReturnExportsOptions } from './_internal' +import type { Filter, FilterResult } from './filters' + +export type GetModuleOptions< + ReturnNamespace extends boolean = boolean, + IncludeUninitialized extends boolean = boolean, +> = WaitForModulesOptions & LookupModulesOptions + +export type GetModuleResult< + F extends Filter, + O extends GetModuleOptions, +> = O extends RunFilterReturnExportsOptions ? MaybeDefaultExportMatched> : FilterResult + +export type GetModuleCallback = (exports: T, id: Metro.ModuleID) => any + +export type GetModuleUnsubscribeFunction = () => boolean + +/** + * Get a module matching the filter. + * + * This is a combination of {@link lookupModule} and {@link waitForModules}. + * + * @param filter The filter to use to find the module. + * @param options The options to use for the find. + * @returns A promise that resolves to the module's exports or rejects if the find is aborted before a module is found. + * + * @example + * ```ts + * getModule(byProps('createElement'), React => { + * // Immediately called because React is always initialized when plugins are loaded + * }) + * + * getModule(byProps('FlashList'), FlashList => { + * // Called when the module is initialized + * }) + * ``` + */ +export function getModule( + filter: F, + callback: GetModuleCallback>, +): GetModuleUnsubscribeFunction + +export function getModule< + F extends O extends GetModuleOptions ? Filter : Filter, + O extends GetModuleOptions, +>(filter: F, callback: GetModuleCallback>, options: O): GetModuleUnsubscribeFunction + +export function getModule(filter: Filter, callback: GetModuleCallback, options?: GetModuleOptions) { + const [exports, id] = lookupModule(filter, options!) + if (id != null) { + callback(exports, id) + return noopFalse + } + + const unsub = waitForModules( + filter, + (id, exports) => { + unsub() + callback(exports, id) + }, + options!, + ) + + return unsub +} + +/** + * Get a module by its imported path. + * + * @param path The path to find the module by. + * @param options The options to use for the find. + * @returns A promise that resolves to the module's exports or rejects if the find is aborted before the module is found. + * + * @example + * ```ts + * getModuleByImportedPath('modules/main_tabs_v2/native/settings/SettingsConstants.tsx', SettingsConstants => { + * console.log('Settings page opened') // Logs once the module is initialized + * }) + * ``` + */ +export function getModuleByImportedPath(path: string, callback: GetModuleCallback): GetModuleUnsubscribeFunction { + const [exports, id] = lookupModuleByImportedPath(path) + if (id != null) { + callback(exports, id) + return noopFalse + } + + const unsub = waitForModuleByImportedPath(path, (exports, id) => { + unsub() + callback(exports, id) + }) + + return unsub +} diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 659b1f0..dcc5b98 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -37,10 +37,12 @@ export type LookupModulesOptions< IncludeUninitialized extends boolean = boolean, > = RunFilterReturnExportsOptions & BaseLookupModulesOptions -export type LookupModulesResult< - F extends Filter, - O extends LookupModulesOptions, -> = O extends RunFilterReturnExportsOptions ? MaybeDefaultExportMatched> : FilterResult +export type LookupModulesResult = [ + exports: O extends RunFilterReturnExportsOptions + ? MaybeDefaultExportMatched> + : FilterResult, + id: Metro.ModuleID, +] export type LookupModuleIdsOptions< IncludeUninitialized extends boolean = boolean, @@ -63,47 +65,6 @@ export type LookupModuleIdsOptions< } & BaseLookupModulesOptions) ) -/** - * Lookup module IDs. - * - * You can lookup uninitialized module IDs by passing `options.includeUninitialized` when filtering via without-exports filters (eg. `byDependencies`). - * Use the `moduleStateAware` helper to filter dynamically based on whether the module is initialized or not. - * - * You can also lookup all modules (even ones with bad exports) by passing `options.includeAll`. - * Use the `preferExports` helper to filter dynamically based on whether the module has good exports or not. - * - * @param filter The filter to use. - * @param options The options to use for the lookup. - * @returns A generator that yields the module exports that match the filter. - * - * @example - * ```ts - * const lookup = lookupModuleIds(byProps('createElement')) - * // Log all module IDs that export React - * for (const id of lookup) console.log(id) - * ``` - * - * @example Lookup uninitialized modules - * ```ts - * const lookup = lookupModuleIds(byDependencies([...]), { includeUninitialized: true }) - * // Log all module IDs that has those dependencies - * for (const id of lookup) console.log(id) - * ``` - */ -export function* lookupModuleIds( - filter: O extends LookupModuleIdsOptions ? Filter : Filter, - options?: O, -): Generator { - if (options?.includeAll) { - for (const id of _metas.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id - } else { - if (options?.includeInitialized ?? true) - for (const id of _inits) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id - - if (options?.includeUninitialized) for (const id of _uninits) if (runFilter(filter, id)) yield id - } -} - /** * Lookup modules. * @@ -121,7 +82,7 @@ export function* lookupModuleIds( * for (const exports of lookup) console.log(exports) * ``` */ -export function lookupModules(filter: F): Generator, undefined> +export function lookupModules(filter: F): Generator, undefined> export function lookupModules< F extends O extends BaseLookupModulesOptions ? Filter : Filter, @@ -133,7 +94,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { for (const id of _inits) { const exports = getInitializedModuleExports(id) const flag = runFilter(filter, id, exports, options) - if (flag) yield exportsFromFilterResultFlag(flag, exports, options) + if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id] } if (options?.includeUninitialized) @@ -142,48 +103,11 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { // Run the filter again to ensure we have the correct exports const exports = __r(id) const flag = runFilter(filter, id, exports, options) - if (flag) yield exportsFromFilterResultFlag(flag, exports, options) - warnDeveloperAboutAPartialFilterMatch(id, filter.key) + if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id] + else warnDeveloperAboutAPartialFilterMatch(id, filter.key) } } -/** - * Lookup a module ID. Skipping creating a `Generator`. - * - * @see {@link lookupModuleIds} for more documentation. - * - * @param filter The filter to use. - * @param options The options to use for the lookup. - * @returns A generator that yields the module exports that match the filter. - * - * @example - * ```ts - * const id = lookupModuleId(byProps('createElement')) - * // Log the first initialized module ID that exports React - * console.log(id) - * ``` - * - * @example Lookup uninitialized modules - * ```ts - * const id = lookupModuleId(byDependencies([...]), { includeUninitialized: true }) - * // Log the first initialized module ID that has those dependencies - * console.log(id) - * ``` - */ -export function lookupModuleId( - filter: O extends BaseLookupModulesOptions ? Filter : Filter, - options?: O, -): Metro.ModuleID | undefined { - if (options?.includeAll) { - for (const id of _metas.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id - } else { - if (options?.includeInitialized ?? true) - for (const id of _inits) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id - - if (options?.includeUninitialized) for (const id of _uninits) if (runFilter(filter, id)) return id - } -} - /** * Lookup a module. Skipping creating a `Generator`. * @@ -198,19 +122,19 @@ export function lookupModuleId( * const React = lookupModule(byProps('createElement')) * ``` */ -export function lookupModule(filter: F): FilterResult | undefined +export function lookupModule(filter: F): LookupModulesResult | [] export function lookupModule( filter: F, options: O, -): LookupModulesResult | undefined +): LookupModulesResult | [] export function lookupModule(filter: Filter, options?: LookupModulesOptions) { if (options?.includeInitialized ?? true) for (const id of _inits) { const exports = getInitializedModuleExports(id) const flag = runFilter(filter, id, exports, options) - if (flag) return exportsFromFilterResultFlag(flag, exports, options) + if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id] } if (options?.includeUninitialized) @@ -219,24 +143,11 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { const exports = __r(id) // Run the filter again to ensure we have the correct exports const flag = runFilter(filter, id, exports, options) - if (flag) return exportsFromFilterResultFlag(flag, exports, options) + if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id] warnDeveloperAboutAPartialFilterMatch(id, filter.key) } -} -/** - * Lookup a module ID by its imported path. **The module may have bad exports.** - * - * @param path The path to lookup the module ID by. - * @returns The module ID if the module is initialized, or `undefined` if the module is not found or not initialized. - * - * @example - * ```ts - * const LoggerId = lookupModuleIdByImportedPath('modules/debug/Logger.tsx') - * ``` - */ -export function lookupModuleIdByImportedPath(path: string): Metro.ModuleID | undefined { - return _paths.get(path) + return [] } /** @@ -249,12 +160,13 @@ export function lookupModuleIdByImportedPath(path: string): Metro.ModuleID | und * * @example * ```ts - * const { default: Logger } = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx') + * const [{ default: Logger }] = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx') * ``` */ -export function lookupModuleByImportedPath(path: string): T | undefined { - // undefined will also return undefined either way, so we can just non-nullish assert it - return getInitializedModuleExports(lookupModuleIdByImportedPath(path)!) +export function lookupModuleByImportedPath(path: string): [exports: T, id: Metro.ModuleID] | [] { + const id = _paths.get(path) + if (id == null) return [] + return [getInitializedModuleExports(id), id] } /** diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index 78cf7de..92a651d 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -15,6 +15,10 @@ export interface BaseWaitForModulesOptions includeAll?: IncludeAll } +export type WaitForModulesUnsubscribeFunction = () => boolean + +export type WaitForModulesCallback = (exports: T, id: Metro.ModuleID) => any + export type WaitForModulesOptions< ReturnNamespace extends boolean = boolean, IncludeBadExports extends boolean = boolean, @@ -38,7 +42,7 @@ export type WaitForModulesResult< * const unsub = waitForModules( * byName('FlashList'), * // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any - * (id, exports) => { + * (exports, id) => { * unsub() * // Do something with the module... * } @@ -47,29 +51,29 @@ export type WaitForModulesResult< */ export function waitForModules( filter: F, - callback: (id: Metro.ModuleID, exports: FilterResult) => any, -): () => void + callback: WaitForModulesCallback>, +): WaitForModulesUnsubscribeFunction export function waitForModules< F extends O extends WaitForModulesOptions ? Filter : Filter, O extends WaitForModulesOptions, ->(filter: F, callback: (id: Metro.ModuleID, exports: WaitForModulesResult) => any, options: O): () => void +>( + filter: F, + callback: WaitForModulesCallback>, + options: O, +): WaitForModulesUnsubscribeFunction -export function waitForModules( - filter: Filter, - callback: (id: Metro.ModuleID, exports: Metro.ModuleExports) => any, - options?: WaitForModulesOptions, -) { +export function waitForModules(filter: Filter, callback: WaitForModulesCallback, options?: WaitForModulesOptions) { return onAnyModuleInitialized( options?.includeAll ? (id, exports) => { const flag = runFilter(filter, id, exports, options) - if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options)) + if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id) } : (id, exports) => { if (initializedModuleHasBadExports(id)) return const flag = runFilter(filter, id, exports, options) - if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options)) + if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id) }, ) } @@ -90,7 +94,7 @@ export function waitForModules( * ```ts * waitForModuleByImportedPath( * 'utils/PlatformUtils.tsx', - * (id, exports) => { + * (exports, id) => { * // Do something with the module... * } * ) @@ -98,7 +102,7 @@ export function waitForModules( */ export function waitForModuleByImportedPath( path: string, - callback: (id: Metro.ModuleID, exports: T) => any, + callback: WaitForModulesCallback, options?: BaseWaitForModulesOptions, ) { const unsub = onModuleFinishedImporting( @@ -106,14 +110,14 @@ export function waitForModuleByImportedPath( ? (id, cmpPath) => { if (path === cmpPath) { unsub() - callback(id, getInitializedModuleExports(id)) + callback(getInitializedModuleExports(id), id) } } : (id, cmpPath) => { if (path === cmpPath) { unsub() if (initializedModuleHasBadExports(id)) return - callback(id, getInitializedModuleExports(id)) + callback(getInitializedModuleExports(id), id) } }, ) diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index e7710ff..d4d03ad 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -5,7 +5,7 @@ import { _execPathSubs } from './subscriptions/_internal' import { byProps } from '../finders/filters' import { waitForModules } from '../finders/wait' -const unsubFFI = waitForModules(byProps('fileFinishedImporting'), (_, exports) => { +const unsubFFI = waitForModules(byProps('fileFinishedImporting'), exports => { unsubFFI() const orig = exports.fileFinishedImporting diff --git a/lib/modules/src/metro/subscriptions/_internal.ts b/lib/modules/src/metro/subscriptions/_internal.ts index cf130a9..123e177 100644 --- a/lib/modules/src/metro/subscriptions/_internal.ts +++ b/lib/modules/src/metro/subscriptions/_internal.ts @@ -1,15 +1,11 @@ -import type { - ModuleFirstRequiredCallback, - ModuleInitializedCallback, - ModuleWithImportedPathInitializedCallback, -} from '.' +import type { ModuleFirstRequiredCallback, ModuleInitializedCallback, ModuleFinishedImportingCallback } from '.' import type { Metro } from '../../types' export const _reqAll = new Set() export const _req = new Map>() export const _initAll = new Set() export const _init = new Map>() -export const _path = new Set() +export const _path = new Set() export function _execReqSubs(id: Metro.ModuleID) { for (const cb of _reqAll) cb(id) diff --git a/lib/modules/src/metro/subscriptions/index.ts b/lib/modules/src/metro/subscriptions/index.ts index 7d8bf6f..079d0f2 100644 --- a/lib/modules/src/metro/subscriptions/index.ts +++ b/lib/modules/src/metro/subscriptions/index.ts @@ -5,11 +5,13 @@ import type { initializedModuleHasBadExports } from '../utils' export type ModuleFirstRequiredCallback = (id: Metro.ModuleID) => void export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void -export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, path: string) => void +export type ModuleFinishedImportingCallback = (id: Metro.ModuleID, path: string) => void /** * Registers a callback to be called when any module is initialized. - * During this time, the module factory is not yet executed. + * + * This runs after the module factory has been executed, but before the module is considered initialized by Metro. + * However, Revenge APIs will consider the module initialized at this point. * * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * @@ -25,7 +27,7 @@ export function onAnyModuleInitialized(callback: ModuleInitializedCallback) { * Registers a callback to be called when a specific module is initialized. * * This runs after the module factory has been executed, but before the module is considered initialized by Metro. - * However, Revenge APIs will consider the module initialized at this poi + * However, Revenge APIs will consider the module initialized at this point. * * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * @@ -52,16 +54,15 @@ export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitiali * @param callback The callback to be called. * @returns A function that unregisters the callback. */ -export function onModuleFinishedImporting(callback: ModuleWithImportedPathInitializedCallback) { +export function onModuleFinishedImporting(callback: ModuleFinishedImportingCallback) { _path.add(callback) return () => _path.delete(callback) } /** - * Registers a callback to be called when any module is required the first time. + * Registers a callback to be called when any module is being initialized. * - * This runs after the module factory has been executed, but before the module is considered initialized by Metro. - * However, Revenge APIs will consider the module initialized at this point. + * This runs before the module factory is executed. * * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * @@ -74,8 +75,9 @@ export function onAnyModuleFirstRequired(callback: ModuleFirstRequiredCallback) } /** - * Registers a callback to be called when a specific module is required the first time. - * During this time, the module factory is not yet executed. + * Registers a callback to be called when a specific module is being initialized. + * + * This runs before the module factory is executed. * * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * diff --git a/lib/plugins/package.json b/lib/plugins/package.json index 7cf4000..662b7d0 100644 --- a/lib/plugins/package.json +++ b/lib/plugins/package.json @@ -9,6 +9,9 @@ "./init": { "default": "./src/init.ts" }, + "./preinit": { + "default": "./src/preinit.ts" + }, "./types": { "default": "./src/types.ts" }, diff --git a/lib/plugins/src/_internal.ts b/lib/plugins/src/_internal.ts index 1e127ed..41dc5fb 100644 --- a/lib/plugins/src/_internal.ts +++ b/lib/plugins/src/_internal.ts @@ -1,6 +1,7 @@ -import { Logger } from '@revenge-mod/discord/common' +import { allSettled, sleep } from '@revenge-mod/utils/promises' import { _uapi } from './apis' + import { PluginFlags as Flag, PluginStatus as Status } from './constants' import type { @@ -8,23 +9,23 @@ import type { Plugin, PluginApi, PluginCleanup, - PluginFlags, PluginLifecycles, PluginManifest, + PreInitPluginApi, } from './types' -const _plugins = new Map() -const _metas = new Map< +export const _plugins = new Map() +export const _metas = new Map< PluginManifest['id'], - [api: InitPluginApi | PluginApi, promises: Promise[], iflags: InternalPluginFlags] + [ + api: PreInitPluginApi | InitPluginApi | PluginApi | undefined, + promises: Promise[], + iflags: number, + apiLevel: number, + ] >() -export function registerPlugin( - manifest: PluginManifest, - lifecycles: PluginLifecycles, - flags: PluginFlags, - iflags: InternalPluginFlags, -) { +export function registerPlugin(manifest: PluginManifest, lifecycles: PluginLifecycles, flags: number, iflags: number) { // TODO(plugins): verify plugin manifest if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`) @@ -35,64 +36,124 @@ export function registerPlugin( lifecycles, status: 0, flags, - async disable() { - // If plugin is not stopped, and is also not stopping, we need to stop it - if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin) - - plugin.flags &= ~Flag.Enabled - }, + disable: () => disablePlugin(plugin), + stop: () => stopPlugin(plugin), } - _metas.set(manifest.id, [ - { - cleanup: (...items) => { - plugin.cleanups.push(...items) - }, - get logger() { - return (this.logger = new Logger(`Revenge > ${manifest.name}`)) - }, - plugin, - unscoped: _uapi, - }, - [], - iflags, - ]) - + _metas.set(manifest.id, [undefined, [], iflags, PluginApiLevel.None]) _plugins.set(manifest.id, plugin) } function handlePluginError(e: unknown, plugin: InternalPlugin) { plugin.errors.push(e) plugin.flags |= Flag.Errored - if (plugin.flags & Flag.Enabled) return plugin.disable() + const [api, , iflags] = _metas.get(plugin.manifest.id)! + + const log = (api as InitPluginApi).logger ?? console + log.error('Plugin encountered an error', e) + + if (!(iflags & InternalPluginFlags.Essential)) return plugin.disable() } -// function preparePluginStart(id: PluginManifest['id']) { -// const api = _metas.get(id)?.[1]! -// api.settings ??= ... -// } +function preparePluginPreInit(id: PluginManifest['id']) { + const plugin = _plugins.get(id)! + const meta = _metas.get(id)! -export function enablePlugin(plugin: InternalPlugin) { + // Clear errors from previous runs + plugin.errors = [] + plugin.status &= ~Flag.Errored + + meta[0] = { + cleanup: (...items) => { + plugin.cleanups.push(...items) + }, + plugin, + unscoped: _uapi, + } + + meta[3] = PluginApiLevel.PreInit +} + +function preparePluginInit(id: PluginManifest['id']) { + const meta = _metas.get(id)! + const api = meta[0] as InitPluginApi + + api.logger = new api.unscoped.discord.common.Logger(`Revenge > Plugins (${id})`) + + meta[3] = PluginApiLevel.Init +} + +function preparePluginStart(id: PluginManifest['id']) { + const meta = _metas.get(id)! + + // const api = meta[0] as PluginApi + // api.settings = ... + + meta[3] = PluginApiLevel.Start +} + +async function disablePlugin(plugin: InternalPlugin) { + const iflags = _metas.get(plugin.manifest.id)?.[2] ?? 0 + if (iflags & InternalPluginFlags.Essential) + throw new Error(`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`) + + // If plugin is not stopped, and is also not stopping, we need to stop it + if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin) + + // TODO(plugins): write to storage + plugin.flags &= ~Flag.Enabled +} + +export function enablePlugin(plugin: InternalPlugin, late: boolean) { // TODO(plugins): write to storage plugin.flags |= Flag.Enabled + if (late) plugin.flags |= Flag.EnabledLate +} + +export async function preInitPlugin(plugin: InternalPlugin) { + const { manifest, lifecycles } = plugin + const meta = _metas.get(manifest.id)! + const [, promises] = meta + + preparePluginPreInit(manifest.id) + + if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`) + if (plugin.status & (Status.PreIniting | Status.PreInited)) + throw new Error(`Plugin preInit lifecycle for "${manifest.id}" is already running`) + + plugin.status |= Status.PreIniting + + try { + const prom = lifecycles.preInit?.(meta[0] as PreInitPluginApi) + promises.push(prom) + await prom + + // plugin.disable() already handles, so it's in the try block + plugin.status |= Status.PreInited + plugin.status &= ~Status.PreIniting + } catch (e) { + await handlePluginError(e, plugin) + } } export async function initPlugin(plugin: InternalPlugin) { const { manifest, lifecycles } = plugin - const [api, promises] = _metas.get(manifest.id)! + const meta = _metas.get(manifest.id)! + const [, promises, , apiLevel] = meta if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`) if (plugin.status & (Status.Initing | Status.Inited)) - throw new Error(`Plugin "${manifest.id}" is initializing or already initialized`) + throw new Error(`Plugin init lifecycle for "${manifest.id}" is already running`) - plugin.status |= Status.Initing + if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id) + if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id) try { - const prom = lifecycles.init?.(api) + const prom = lifecycles.init?.(meta[0] as InitPluginApi) promises.push(prom) await prom - // plugin.disable() already handles, so it's the try block + // plugin.disable() already handles, so it's in the try block plugin.status |= Status.Inited plugin.status &= ~Status.Initing } catch (e) { @@ -102,22 +163,21 @@ export async function initPlugin(plugin: InternalPlugin) { export async function startPlugin(plugin: InternalPlugin) { const { manifest, lifecycles } = plugin - const [api, promises] = _metas.get(manifest.id)! + const meta = _metas.get(manifest.id)! + const [, promises, , apiLevel] = meta if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`) if (plugin.status & (Status.Starting | Status.Started)) - throw new Error(`Plugin "${manifest.id}" is starting or already started`) - - // Clear errors from previous runs - plugin.errors = [] - plugin.status &= ~Flag.Errored + throw new Error(`Plugin start lifecycle for "${manifest.id}" is already running`) plugin.status |= Status.Starting - // preparePluginStart(manifest.id) + if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id) + if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id) + if (apiLevel < PluginApiLevel.Start) preparePluginStart(manifest.id) try { - const prom = lifecycles.start?.(api as PluginApi) + const prom = lifecycles.start?.(meta[0] as PluginApi) promises.push(prom) await prom @@ -129,31 +189,42 @@ export async function startPlugin(plugin: InternalPlugin) { } } -async function stopPlugin(plugin: InternalPlugin) { +const MaxLifecycleWaitTime = 5000 + +export async function stopPlugin(plugin: InternalPlugin) { const { manifest, lifecycles } = plugin const meta = _metas.get(manifest.id)! - const [api, promises] = meta + const [, promises, iflags, apiLevel] = meta + + if (iflags & InternalPluginFlags.Essential) + throw new Error(`Plugin "${manifest.id}" is essential and cannot be stopped`) if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`) if (plugin.status & Status.Stopping) throw new Error(`Plugin "${manifest.id}" is stopping`) - // If the plugin is initializing or starting, we need to wait for it to finish, then we'll stop it - if (plugin.status & (Status.Initing | Status.Starting)) await Promise.all(promises) - if (plugin.status & (Status.Inited | Status.Started)) - throw new Error(`Plugin "${manifest.id}" is not initialized started`) + // If the plugin is running its lifecycles, we need to wait for it to finish, then we'll stop it + // We want to wait at max 5 seconds for the lifecycles to finish + if (plugin.status & (Status.PreIniting | Status.Initing | Status.Starting)) + await Promise.race([Promise.all(promises), sleep(MaxLifecycleWaitTime)]).then(finished => { + // If the lifecycles don't finish in 5 seconds, a reload is probably required to unapply the changes + if (!finished) plugin.flags |= Flag.ReloadRequired + }) + else if (!(plugin.status & (Status.PreInited | Status.Inited | Status.Started))) + throw new Error(`Plugin "${manifest.id}" is not running`) - // In case the plugin only has init() and stop() - // preparePluginStart(manifest.id) + if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id) + if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id) + if (apiLevel < PluginApiLevel.Start) preparePluginStart(manifest.id) plugin.status |= Status.Stopping try { - await lifecycles.stop?.(api as PluginApi) + await lifecycles.stop?.(meta[0] as PluginApi) } catch (e) { await handlePluginError(e, plugin) } finally { // Run cleanups - const results = await Promise.allSettled(plugin.cleanups.map(cleanup => cleanup())) + const results = await allSettled(plugin.cleanups.map(cleanup => cleanup())) for (const result of results) if (result.status === 'rejected') { await handlePluginError(result.reason, plugin) @@ -161,33 +232,19 @@ async function stopPlugin(plugin: InternalPlugin) { plugin.flags |= Flag.ReloadRequired } - // Clear unnecessary data + // Reset APIs + meta[0] = undefined + meta[3] = PluginApiLevel.None + + // Clear temp data meta[1] = [] plugin.cleanups = [] + + // Reset status plugin.status = 0 } } -export function getEnabledPluginsCount() { - let count = 0 - for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) count++ - return count -} - -export async function initPlugins() { - const promises: Promise[] = [] - for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(initPlugin(plugin)) - // These Promises never throw, so it is fine - await Promise.all(promises) -} - -export async function startPlugins() { - const promises: Promise[] = [] - for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(startPlugin(plugin)) - // These Promises never throw, so it is fine - await Promise.all(promises) -} - export interface InternalPlugin extends Plugin { cleanups: PluginCleanup[] } @@ -203,4 +260,9 @@ export const InternalPluginFlags = { Essential: 1 << 1, } -export type InternalPluginFlags = (typeof InternalPluginFlags)[keyof typeof InternalPluginFlags] +const PluginApiLevel = { + None: 0, + PreInit: 1, + Init: 2, + Start: 3, +} as const diff --git a/lib/plugins/src/apis/index.ts b/lib/plugins/src/apis/index.ts index 268f407..49c58f0 100644 --- a/lib/plugins/src/apis/index.ts +++ b/lib/plugins/src/apis/index.ts @@ -1,11 +1,8 @@ import * as PatcherLibrary from '@revenge-mod/patcher' -import type { UnscopedInitPluginApi, UnscopedPluginApi } from '../types' +import type { UnscopedInitPluginApi, UnscopedPluginApi, UnscopedPreInitPluginApi } from '../types' -export const _uapi: UnscopedInitPluginApi | UnscopedPluginApi = { - discord: require('./discord').discord, - externals: require('./externals').externals, +export const _uapi: UnscopedPreInitPluginApi | UnscopedInitPluginApi | UnscopedPluginApi = { modules: require('./modules').modules, plugins: require('./plugins').plugins, patcher: PatcherLibrary, - react: require('./react').react, } diff --git a/lib/plugins/src/apis/modules.ts b/lib/plugins/src/apis/modules.ts index df4fa53..015edc9 100644 --- a/lib/plugins/src/apis/modules.ts +++ b/lib/plugins/src/apis/modules.ts @@ -13,6 +13,7 @@ export type PluginApiModulesMetro = export type PluginApiModulesFinders = // biome-ignore format: Don't + typeof import('@revenge-mod/modules/finders/get') & typeof import('@revenge-mod/modules/finders/lookup') & typeof import('@revenge-mod/modules/finders/wait') & { filters: typeof import('@revenge-mod/modules/finders/filters') @@ -20,6 +21,7 @@ export type PluginApiModulesFinders = export const modules: PluginApiModules = { finders: { + ...require('@revenge-mod/modules/finders/get'), ...require('@revenge-mod/modules/finders/lookup'), ...require('@revenge-mod/modules/finders/wait'), filters: require('@revenge-mod/modules/finders/filters'), diff --git a/lib/plugins/src/apis/react.ts b/lib/plugins/src/apis/react.ts index 148ed90..cc1fbf4 100644 --- a/lib/plugins/src/apis/react.ts +++ b/lib/plugins/src/apis/react.ts @@ -1,8 +1,10 @@ export type PluginApiReact = typeof import('@revenge-mod/react') & { jsxRuntime: typeof import('@revenge-mod/react/jsx-runtime') + native: typeof import('@revenge-mod/react/native') } export const react: PluginApiReact = { ...require('@revenge-mod/react'), jsxRuntime: require('@revenge-mod/react/jsx-runtime'), + native: require('@revenge-mod/react/native'), } diff --git a/lib/plugins/src/constants.ts b/lib/plugins/src/constants.ts index fbc8cce..eb77332 100644 --- a/lib/plugins/src/constants.ts +++ b/lib/plugins/src/constants.ts @@ -11,12 +11,19 @@ export const PluginFlags = { * The plugin has errors. */ Errored: 1 << 2, + /** + * The plugin was enabled after the app was started. + * This is usually caused by a newly installed plugin, or a plugin that was re-enabled. + */ + EnabledLate: 1 << 3, } export const PluginStatus = { - Initing: 1 << 0, - Inited: 1 << 1, - Starting: 1 << 2, - Started: 1 << 3, - Stopping: 1 << 4, + PreIniting: 1 << 0, + PreInited: 1 << 1, + Initing: 1 << 2, + Inited: 1 << 3, + Starting: 1 << 4, + Started: 1 << 5, + Stopping: 1 << 6, } diff --git a/lib/plugins/src/init.ts b/lib/plugins/src/init.ts index 5726656..1bf30dd 100644 --- a/lib/plugins/src/init.ts +++ b/lib/plugins/src/init.ts @@ -1,25 +1,32 @@ -import * as AssetsLibrary from '@revenge-mod/assets' -// import * as UiLibrary from '@revenge-mod/ui' +import { onRunApplication } from '@revenge-mod/react/native' -import { after } from '@revenge-mod/patcher' -import { ReactNative } from '@revenge-mod/react' - -import { initPlugins, startPlugins } from './_internal' import { _uapi } from './apis' -import type { UnscopedPluginApi } from './types' +import * as AssetsLibrary from '@revenge-mod/assets' +import { discord } from './apis/discord' +import { externals } from './apis/externals' +import { react } from './apis/react' -// Setup non-limited APIs -const uapi = _uapi as UnscopedPluginApi +import { _plugins, initPlugin, startPlugin } from './_internal' +import { PluginFlags } from './constants' + +import type { UnscopedInitPluginApi } from './types' + +// Setup init plugin APIs +const uapi = _uapi as UnscopedInitPluginApi uapi.assets = AssetsLibrary -// uapi.ui = UiLibrary +uapi.discord = discord +uapi.externals = externals +uapi.react = react -initPlugins() +for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin) -// TODO(plugins/init): move to @revenge-mod/react/native onRunApplicationCalled -const unpatch = after(ReactNative.AppRegistry, 'runApplication', ret => { - unpatch() - // Don't block rendering - setImmediate(startPlugins) - return ret +const unsub = onRunApplication(() => { + unsub() + + // TODO(plugins/start): init start APIs + // const UILibrary = require('@revenge-mod/ui') + // uapi.ui = UiLibrary + + for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin) }) diff --git a/lib/plugins/src/preinit.ts b/lib/plugins/src/preinit.ts new file mode 100644 index 0000000..3d6654a --- /dev/null +++ b/lib/plugins/src/preinit.ts @@ -0,0 +1,4 @@ +import { _plugins, preInitPlugin } from './_internal' +import { PluginFlags } from './constants' + +for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin) diff --git a/lib/plugins/src/types.ts b/lib/plugins/src/types.ts index fa58e54..5feb02e 100644 --- a/lib/plugins/src/types.ts +++ b/lib/plugins/src/types.ts @@ -1,6 +1,6 @@ -import type { DiscordModules } from '@revenge-mod/discord/types' +import type { PluginStatus, PluginFlags } from './constants' -import type { PluginFlags as PF, PluginStatus as PS } from './constants' +import type { DiscordModules } from '@revenge-mod/discord/types' import type { PluginApiDiscord } from './apis/discord' import type { PluginApiExternals } from './apis/externals' @@ -8,21 +8,26 @@ import type { PluginApiModules } from './apis/modules' import type { PluginApiReact } from './apis/react' import type { PluginApiPlugins } from './apis/plugins' -export type PluginFlags = (typeof PF)[keyof typeof PF] -export type PluginStatus = (typeof PS)[keyof typeof PS] +/** + * The unscoped plugin API (very limited). This API is available as a global for plugins. + * Available in the `preInit` phase. + */ +export interface UnscopedPreInitPluginApi { + modules: PluginApiModules + patcher: typeof import('@revenge-mod/patcher') + plugins: PluginApiPlugins + // utils: PluginApiUtils +} /** * The unscoped plugin API (limited). This API is available as a global for plugins. * Available in the `init` phase. */ -export interface UnscopedInitPluginApi { +export interface UnscopedInitPluginApi extends UnscopedPreInitPluginApi { + assets: typeof import('@revenge-mod/assets') discord: PluginApiDiscord externals: PluginApiExternals - modules: PluginApiModules - patcher: typeof import('@revenge-mod/patcher') - plugins: PluginApiPlugins react: PluginApiReact - // utils: PluginApiUtils } /** @@ -30,22 +35,29 @@ export interface UnscopedInitPluginApi { * Available in the `start` and `stop` phase. */ export interface UnscopedPluginApi extends UnscopedInitPluginApi { - assets: typeof import('@revenge-mod/assets') // ui: typeof import('@revenge-mod/ui') } export type PluginCleanup = () => any export type PluginCleanupApi = (...fns: PluginCleanup[]) => void +/** + * The plugin API (very limited). + * Available in the `preInit` phase. + */ +export interface PreInitPluginApi { + unscoped: UnscopedPreInitPluginApi + cleanup: PluginCleanupApi + plugin: Plugin +} + /** * The plugin API (limited). * Available in the `init` phase. */ -export interface InitPluginApi { - plugin: Plugin - logger: InstanceType +export interface InitPluginApi extends PreInitPluginApi { unscoped: UnscopedInitPluginApi - cleanup: PluginCleanupApi + logger: InstanceType } /** @@ -98,24 +110,29 @@ export interface PluginManifest { export interface PluginLifecycles { /** - * Runs immediately as soon as possible with limited APIs. + * Runs as soon as possible with very limited APIs. + * Before the index module (module 0)'s factory is run. + * + * @param api Plugin API (very limited). + */ + preInit?: (api: PreInitPluginApi) => any + /** + * Runs as soon as all important modules are initialized. + * After the index module (module 0)'s factory is run. * * @param api Plugin API (limited). - * @param context Plugin context. */ init?: (api: InitPluginApi) => any /** * Runs when the plugin can be started with all APIs available. * * @param api Plugin API. - * @param context Plugin context. */ start?: (api: PluginApi) => any /** * Runs when the plugin is stopped. * * @param api Plugin API. - * @param context Plugin context. */ stop?: (api: PluginApi) => any } @@ -137,12 +154,14 @@ export interface Plugin { /** * The plugin flags. + * @see {@link PluginFlags} */ - flags: PluginFlags + flags: number /** * The plugin status. + * @see {@link PluginStatus} */ - status: PluginStatus + status: number /** * Errors encountered during the plugin lifecycles. */ @@ -152,4 +171,8 @@ export interface Plugin { * Disable the plugin. This will also stop the plugin if it is running. */ disable(): Promise + /** + * Stop the plugin. + */ + stop(): Promise } diff --git a/lib/react/package.json b/lib/react/package.json index 33c528f..78b0a3f 100644 --- a/lib/react/package.json +++ b/lib/react/package.json @@ -9,6 +9,9 @@ "./jsx-runtime": { "default": "./src/jsx-runtime/index.ts" }, + "./native": { + "default": "./src/native/index.ts" + }, "./preinit": { "default": "./src/preinit.ts" }, diff --git a/lib/react/src/index.ts b/lib/react/src/index.ts index ad46fb2..b303e98 100644 --- a/lib/react/src/index.ts +++ b/lib/react/src/index.ts @@ -1,11 +1,37 @@ import { byProps } from '@revenge-mod/modules/finders/filters' -import { lookupModuleId, lookupModuleIds } from '@revenge-mod/modules/finders/lookup' -import { yielded } from '@revenge-mod/utils/generators' +import { waitForModules } from '@revenge-mod/modules/finders/wait' -export const ReactModuleId = yielded(lookupModuleIds(byProps('useState')), 1)! -export const ReactNativeModuleId = lookupModuleId(byProps('AppRegistry'))! -export const ReactJsxRuntimeModuleId = yielded(lookupModuleIds(byProps('jsx'))!, 1) +import type { Metro } from '@revenge-mod/modules/types' -export const React = __r(ReactModuleId) as typeof import('react') -export const ReactNative = __r(ReactNativeModuleId) as typeof import('react-native') -export const ReactJsxRuntime = __r(ReactJsxRuntimeModuleId) as typeof import('react/jsx-runtime') +export let ReactModuleId: Metro.ModuleID +export let ReactNativeModuleId: Metro.ModuleID +export let ReactJsxRuntimeModuleId: Metro.ModuleID + +export let React: typeof import('react') +export let ReactNative: typeof import('react-native') +export let ReactJsxRuntime: typeof import('react/jsx-runtime') + +let RtCount = 0 + +const unsubRt = waitForModules(byProps('useState'), (exports, id) => { + if (RtCount++ === 2) return unsubRt() + + ReactModuleId = id + React = exports +}) + +const unsubRN = waitForModules(byProps('AppRegistry'), (exports, id) => { + unsubRN() + + ReactNativeModuleId = id + ReactNative = exports +}) + +let RJsxRCount = 0 + +const unsubRJsxR = waitForModules(byProps('jsxs'), (exports, id) => { + if (RJsxRCount++ === 2) return unsubRJsxR() + + ReactJsxRuntimeModuleId = id + ReactJsxRuntime = exports +}) diff --git a/lib/react/src/native/index.ts b/lib/react/src/native/index.ts new file mode 100644 index 0000000..e219487 --- /dev/null +++ b/lib/react/src/native/index.ts @@ -0,0 +1,27 @@ +import { _ara, _bra } from './patches' + +import type { AppRegistry } from 'react-native' + +/** + * Registers a callback to be run when a call to {@link AppRegistry.runApplication} is made. + * + * @param callback The callback to be called. + * @returns A function to unregister the callback. + */ +export function onRunApplication(callback: RunApplicationCallback) { + _bra.add(callback) + return () => _bra.delete(callback) +} + +/** + * Registers a callback to be run when a call to {@link AppRegistry.runApplication} is finished. + * + * @param callback The callback to be called. + * @returns A function to unregister the callback. + */ +export function onRunApplicationFinished(callback: RunApplicationCallback) { + _ara.add(callback) + return () => _ara.delete(callback) +} + +export type RunApplicationCallback = () => any diff --git a/lib/react/src/native/patches.ts b/lib/react/src/native/patches.ts new file mode 100644 index 0000000..83a65b8 --- /dev/null +++ b/lib/react/src/native/patches.ts @@ -0,0 +1,24 @@ +import { after, before } from '@revenge-mod/patcher' +import { ReactNative } from '..' + +import { waitForModules } from '@revenge-mod/modules/finders/wait' +import { byProps } from '@revenge-mod/modules/finders/filters' + +import type { RunApplicationCallback } from '.' + +export const _bra = new Set() +export const _ara = new Set() + +const unsub = waitForModules(byProps('AppRegistry'), () => { + unsub() + + before(ReactNative.AppRegistry, 'runApplication', args => { + for (const cb of _bra) cb() + return args + }) + + after(ReactNative.AppRegistry, 'runApplication', res => { + for (const cb of _ara) cb() + return res + }) +}) diff --git a/lib/react/src/preinit.ts b/lib/react/src/preinit.ts index 1c7c61e..cea1efd 100644 --- a/lib/react/src/preinit.ts +++ b/lib/react/src/preinit.ts @@ -1 +1,5 @@ +// Initialize the wait immediately, so we don't miss the actual modules before the re-exports +import '.' + +import './native/patches' import './jsx-runtime/patches' diff --git a/lib/storage/package.json b/lib/storage/package.json new file mode 100644 index 0000000..84fbaef --- /dev/null +++ b/lib/storage/package.json @@ -0,0 +1,6 @@ +{ + "name": "@revenge-mod/storage", + "private": true, + "type": "module", + "exports": {} +} diff --git a/lib/storage/src/index.ts b/lib/storage/src/index.ts new file mode 100644 index 0000000..cdbdc6d --- /dev/null +++ b/lib/storage/src/index.ts @@ -0,0 +1 @@ +// TODO(storage): Implement from NativeModules interop instead diff --git a/lib/utils/src/callbacks.ts b/lib/utils/src/callbacks.ts index 257be6f..09dddfa 100644 --- a/lib/utils/src/callbacks.ts +++ b/lib/utils/src/callbacks.ts @@ -8,3 +8,5 @@ export function debounce any>(func: F, timeout: nu }, timeout) }) } + +export const noopFalse = () => false diff --git a/lib/utils/src/discord.ts b/lib/utils/src/discord.ts index 6cc1e1f..0b98e37 100644 --- a/lib/utils/src/discord.ts +++ b/lib/utils/src/discord.ts @@ -25,7 +25,7 @@ const MultiIconComponentFilterBase = [ // TODO(utils/discord/lookupGeneratedIconComponent): Make this into a filter? export function lookupGeneratedIconComponent(compName: N, ...assetNames: string[]) { - const icon = lookupModule(byProps(compName))?.[compName] + const icon = lookupModule(byProps(compName))[0]?.[compName] if (icon) return icon let filter = IconComponentFilter @@ -51,5 +51,5 @@ export function lookupGeneratedIconComponent(compName: N, ...a return lookupModule(byDependencies<{ [K in N]: FC }>(filter), { includeInitialized: false, includeUninitialized: true, - })?.[compName] + })[0]?.[compName] } diff --git a/lib/utils/src/promises.ts b/lib/utils/src/promises.ts index c202b6b..966ea3b 100644 --- a/lib/utils/src/promises.ts +++ b/lib/utils/src/promises.ts @@ -1,21 +1,30 @@ export function allSettled(promises: Promise[]): Promise[]> { const mapped = promises.map(p => - p - ?.then( - value => - ({ - status: 'fulfilled', - value, - }) as const, - ) - .catch( - reason => - ({ - status: 'rejected', - reason, - }) as const, - ), + p instanceof Promise + ? p + .then( + value => + ({ + status: 'fulfilled', + value, + }) as const, + ) + .catch( + reason => + ({ + status: 'rejected', + reason, + }) as const, + ) + : ({ + status: 'fulfilled', + value: p, + } as const), ) return Promise.all(mapped) } + +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/src/components/FormSwitch.tsx b/src/components/FormSwitch.tsx new file mode 100644 index 0000000..0b930df --- /dev/null +++ b/src/components/FormSwitch.tsx @@ -0,0 +1,21 @@ +import { FormSwitch as Switch } from '@revenge-mod/discord/design' +import { ReactNative } from '@revenge-mod/react' + +import type { DiscordModules } from '@revenge-mod/discord/types' + +/** + * A switch component that is styled to match Discord's configuration + */ +export default function FormSwitch(props: DiscordModules.Components.FormSwitchProps) { + return ( + + + + ) +} + +const styles = ReactNative.StyleSheet.create({ + disabled: { + opacity: 0.5, + }, +}) diff --git a/src/index.ts b/src/index.ts index e7c0578..333bcd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,58 @@ -import '@revenge-mod/init' +// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below. +import { patchMetroDefine } from '@revenge-mod/modules/_/metro' +import { interceptProperty } from '@revenge-mod/utils/objects' + +// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set. +// But both should be true either way. +const unintercept = interceptProperty('__d', (_, mDefine) => (unintercept(), patchMetroDefine(mDefine))) + +import { onModuleFirstRequired, onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions' +import { getErrorStack } from '@revenge-mod/utils/errors' + +const IndexModuleId = 0 + +onModuleFirstRequired(IndexModuleId, function onIndexRequired() { + runCatching(() => { + if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections') + + // Initialize preinit libraries + require('@revenge-mod/modules/preinit') + require('@revenge-mod/react/preinit') + require('@revenge-mod/assets/preinit') + require('@revenge-mod/discord/preinit') + + // Run all preinit plugins + require('~/plugins/_preinit') + require('@revenge-mod/plugins/preinit') + + onModuleInitialized(IndexModuleId, function onIndexInitialized() { + runCatching(() => { + // Initialize init libraries + + // Run all init plugins + require('~/plugins/_all') + require('@revenge-mod/plugins/init') + }) + }) + }) +}) + +function runCatching(fn: () => void) { + try { + fn() + } catch (e) { + // TODO(init): Move to use native provided alert function, which will accept a string stack trace + // Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further + const { ClientInfoModule, DeviceModule } = require('@revenge-mod/discord/native') as + // biome-ignore format: Don't format this please + typeof import('@revenge-mod/discord/native') + + alert( + // biome-ignore lint/style/useTemplate: I want this to be readable, thank you + `Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` + + `Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})\n` + + `Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}\n\n` + + getErrorStack(e), + ) + } +} diff --git a/lib/init/src/patches/log-promise-rejections.ts b/src/patches/log-promise-rejections.ts similarity index 93% rename from lib/init/src/patches/log-promise-rejections.ts rename to src/patches/log-promise-rejections.ts index a259c18..55eca65 100644 --- a/lib/init/src/patches/log-promise-rejections.ts +++ b/src/patches/log-promise-rejections.ts @@ -8,7 +8,7 @@ import { getErrorStack } from '@revenge-mod/utils/errors' import { createLogger } from '@revenge-mod/utils/logger' -const logger = createLogger('revenge.init.promise-rejections') +const logger = createLogger('revenge.promise-rejections') const ErrorTypeWhitelist = [ReferenceError, TypeError, RangeError] diff --git a/src/plugins/all.ts b/src/plugins/_all.ts similarity index 80% rename from src/plugins/all.ts rename to src/plugins/_all.ts index 2c9f806..4007c77 100644 --- a/src/plugins/all.ts +++ b/src/plugins/_all.ts @@ -1,4 +1,3 @@ -import './logging' import './staff-settings' import './settings' import './developer-settings' diff --git a/src/plugins/_preinit.ts b/src/plugins/_preinit.ts new file mode 100644 index 0000000..da73cd2 --- /dev/null +++ b/src/plugins/_preinit.ts @@ -0,0 +1,2 @@ +import './logging' +import './no-track' diff --git a/src/plugins/developer-settings/definitions/EvaluateJavaScriptSetting.tsx b/src/plugins/developer-settings/definitions/EvaluateJavaScriptSetting.tsx index 37037a2..96a1b41 100644 --- a/src/plugins/developer-settings/definitions/EvaluateJavaScriptSetting.tsx +++ b/src/plugins/developer-settings/definitions/EvaluateJavaScriptSetting.tsx @@ -9,7 +9,7 @@ import { getErrorStack } from '@revenge-mod/utils/errors' import TableRowAssetIcon from '~/components/TableRowAssetIcon' -import { pluginApi } from '..' +import { api } from '..' import { MobileSetting } from '../constants' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' @@ -98,14 +98,14 @@ function EvaluateJavaScriptAlert() { setLoading(true) try { - if (!pluginApi) { + if (!api) { alert('Unable to provide plugin API. Running snippet in a second...') await new Promise(rs => setTimeout(rs, 1000)) } else { // @ts-expect-error - globalThis.revenge = pluginApi.unscoped + globalThis.revenge = api.unscoped // @ts-expect-error - globalThis.api = pluginApi + globalThis.api = api } // Do a no-local-scope-access eval diff --git a/src/plugins/developer-settings/devtools.ts b/src/plugins/developer-settings/devtools.ts index 843d488..c84ad02 100644 --- a/src/plugins/developer-settings/devtools.ts +++ b/src/plugins/developer-settings/devtools.ts @@ -2,7 +2,7 @@ import { ToastActionCreators } from '@revenge-mod/discord/actions' import { React } from '@revenge-mod/react' import { getErrorStack } from '@revenge-mod/utils/errors' import { useReRender } from '@revenge-mod/utils/react' -import { pluginApi } from '.' +import { api } from '.' import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord' export const DevToolsContext: { @@ -26,9 +26,7 @@ subscriptions.add((e, err) => { if (e === 3) { const actualError = (err as { message: string }).message ?? getErrorStack(err) - pluginApi.logger.error('React DevTools error:', actualError) - - pluginApi.logger.log(CircleXIcon) + api.logger.error('React DevTools error:', actualError) ToastActionCreators.open({ key: 'DEVTOOLS_ERROR', diff --git a/src/plugins/developer-settings/index.ts b/src/plugins/developer-settings/index.ts index 07626b6..1dba2d9 100644 --- a/src/plugins/developer-settings/index.ts +++ b/src/plugins/developer-settings/index.ts @@ -2,7 +2,7 @@ import { PluginFlags } from '@revenge-mod/plugins/constants' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' import { _data } from '@revenge-mod/discord/_/modules/settings' -import { onceSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings' +import { isSettingsModulesLoaded, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings' import type { PluginApi } from '@revenge-mod/plugins/types' @@ -15,9 +15,10 @@ registerPlugin( icon: 'WrenchIcon', }, { - start(api) { - pluginApi = api - onceSettingsModulesLoaded(() => require('./register')) + start(api_) { + api = api_ + if (isSettingsModulesLoaded()) require('./register').register() + else onSettingsModulesLoaded(() => require('./register').register()) }, }, PluginFlags.Enabled, @@ -25,4 +26,4 @@ registerPlugin( ) // Expose to EvaluateJavaScriptSetting -export let pluginApi: PluginApi +export let api: PluginApi diff --git a/src/plugins/developer-settings/register.ts b/src/plugins/developer-settings/register.ts index 431cb74..0fdd7a0 100644 --- a/src/plugins/developer-settings/register.ts +++ b/src/plugins/developer-settings/register.ts @@ -9,13 +9,18 @@ import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetti import RevengeDeveloperSetting from './definitions/RevengeDeveloperSetting' import AssetBrowserSetting from './definitions/AssetBrowserSetting' -registerSettingsItems({ - [MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting, - [MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting, - [MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting, - [MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting, - [MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting, - [MobileSetting.ASSET_BROWSER]: AssetBrowserSetting, -}) +import { api } from '.' -addSettingsItemToSection('REVENGE', MobileSetting.REVENGE_DEVELOPER) +export function register() { + api.cleanup( + registerSettingsItems({ + [MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting, + [MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting, + [MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting, + [MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting, + [MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting, + [MobileSetting.ASSET_BROWSER]: AssetBrowserSetting, + }), + addSettingsItemToSection('REVENGE', MobileSetting.REVENGE_DEVELOPER), + ) +} diff --git a/src/plugins/logging/index.ts b/src/plugins/logging/index.ts index 7e98bd9..b100742 100644 --- a/src/plugins/logging/index.ts +++ b/src/plugins/logging/index.ts @@ -1,4 +1,4 @@ -import { AppStartPerformance } from '@revenge-mod/discord/common' +import { AppStartPerformance } from '@revenge-mod/discord/preinit' import { PluginFlags } from '@revenge-mod/plugins/constants' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' @@ -6,6 +6,7 @@ import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' AppStartPerformance.mark('👊', 'Plugins register') const tsReg = performance.now() +let tsPreInit: number let tsInit: number registerPlugin( @@ -17,9 +18,13 @@ registerPlugin( icon: 'PaperIcon', }, { + preInit() { + tsPreInit = performance.now() + AppStartPerformance.mark('👊', 'Plugins preInit', tsPreInit - tsReg) + }, init() { tsInit = performance.now() - AppStartPerformance.mark('👊', 'Plugins init', tsInit - tsReg) + AppStartPerformance.mark('👊', 'Plugins init', tsInit - tsPreInit) }, start({ logger }) { AppStartPerformance.mark('👊', 'Plugins start', performance.now() - tsInit) diff --git a/src/plugins/no-track/index.ts b/src/plugins/no-track/index.ts new file mode 100644 index 0000000..be8eaf9 --- /dev/null +++ b/src/plugins/no-track/index.ts @@ -0,0 +1,69 @@ +import { byProps } from '@revenge-mod/modules/finders/filters' +import { waitForModules } from '@revenge-mod/modules/finders/wait' + +import { instead } from '@revenge-mod/patcher' + +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' + +// TODO(plugins/no-track): Block Sentry native-side +registerPlugin( + { + id: 'revenge.no-track', + name: 'No Track', + description: "Disables Discord's analytics/tracking, and Sentry.", + author: 'Revenge', + icon: 'AnalyticsIcon', + }, + { + preInit({ cleanup }) { + // modules/errors/native/SentryInitUtils.tsx + const unsubSIU = waitForModules(byProps('initSentry'), SentryInitUtils => { + unsubSIU() + + console.log('Patching SentryInitUtils...') + cleanup(instead(SentryInitUtils, 'initSentry', () => {})) + cleanup(() => SentryInitUtils.initSentry()) + }) + }, + init({ cleanup, logger }) { + // utils/AnalyticsUtils.tsx + const unsubAU = waitForModules( + byProps<{ + trackNetworkAction: () => void + default: { + track: () => void + AnalyticsActionHandlers: Record void> + } + }>('trackNetworkAction'), + AnalyticsUtils => { + unsubAU() + + logger.info('Patching AnalyticsUtils...') + cleanup(instead(AnalyticsUtils.default, 'track', () => {})) + cleanup(instead(AnalyticsUtils, 'trackNetworkAction', () => {})) + + for (const key in AnalyticsUtils.default.AnalyticsActionHandlers) + cleanup(instead(AnalyticsUtils.default.AnalyticsActionHandlers, key, () => {})) + }, + ) + // modules/app_analytics/useTrackImpression.tsx + const unsubTI = waitForModules( + byProps<{ + default: () => void + trackImpression: () => void + }>('trackImpression'), + useTrackImpression => { + unsubTI() + + logger.info('Patching useTrackImpression...') + cleanup(instead(useTrackImpression, 'trackImpression', () => {})) + cleanup(instead(useTrackImpression, 'default', () => {})) + }, + ) + }, + }, + PluginFlags.Enabled, + // TODO(plugins/no-track): is it essential? + InternalPluginFlags.Internal | InternalPluginFlags.Essential, +) diff --git a/src/plugins/settings/definitions/RevengePluginsSetting.tsx b/src/plugins/settings/definitions/RevengePluginsSetting.tsx index 5c5f58e..24ee99e 100644 --- a/src/plugins/settings/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/settings/definitions/RevengePluginsSetting.tsx @@ -1,4 +1,5 @@ -import { getEnabledPluginsCount } from '@revenge-mod/plugins/_' +import { _plugins } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' import TableRowAssetIcon from '~/components/TableRowAssetIcon' @@ -11,7 +12,11 @@ const RevengePluginsSetting: SettingsItem = { type: 'route', IconComponent: () => , title: () => 'Plugins', - useTrailing: () => `${getEnabledPluginsCount()} enabled`, + useTrailing: () => { + let count = 0 + for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) count++ + return `${count} enabled` + }, screen: { route: UserSettingsSections[MobileSetting.REVENGE_PLUGINS], getComponent: () => require('../screens/RevengePluginsSettingScreen').default, diff --git a/src/plugins/settings/index.ts b/src/plugins/settings/index.ts index b287f34..f64cd03 100644 --- a/src/plugins/settings/index.ts +++ b/src/plugins/settings/index.ts @@ -5,7 +5,7 @@ import { byName, byProps } from '@revenge-mod/modules/finders/filters' import { waitForModules } from '@revenge-mod/modules/finders/wait' import { _data, _subs } from '@revenge-mod/discord/_/modules/settings' -import { type SettingsItem, onceSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings' +import { type SettingsItem, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings' import { after } from '@revenge-mod/patcher' @@ -21,13 +21,13 @@ registerPlugin( }, { start({ logger }) { - onceSettingsModulesLoaded(() => require('./register')) + onSettingsModulesLoaded(() => require('./register')) const unsubForRendererConfig = waitForModules( byProps<{ SETTING_RENDERER_CONFIG: Record }>('SETTING_RENDERER_CONFIG'), - (_, SettingRendererConfig) => { + SettingRendererConfig => { unsubForRendererConfig() logger.info('Settings modules loaded, running subscriptions and patching...') @@ -52,16 +52,22 @@ registerPlugin( const unsubForSettingsOverviewScreen = waitForModules( byName('SettingsOverviewScreen'), - (_, exports) => { + exports => { unsubForSettingsOverviewScreen() - after(exports as { default: FC }, 'default', tree => { - const sections = (tree as Extract).props.sections as Array<{ - label: string - settings: string[] - }> + const customSections = _data[0] - const customSections = _data[0] + after(exports as { default: FC }, 'default', tree => { + const { + props: { sections }, + } = tree as { + props: { + sections: Array<{ + label: string + settings: string[] + }> + } + } // Check if we even have custom sections const firstCustomSection = customSections[Object.keys(customSections)[0]] diff --git a/src/plugins/settings/screens/RevengePluginsSettingScreen.tsx b/src/plugins/settings/screens/RevengePluginsSettingScreen.tsx index 25675f1..b1d5104 100644 --- a/src/plugins/settings/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/settings/screens/RevengePluginsSettingScreen.tsx @@ -1,15 +1,154 @@ -import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer' +import { Design } from '@revenge-mod/discord/design' +import { FlashList } from '@revenge-mod/externals/shopify' +import { React, ReactNative } from '@revenge-mod/react' +import { + _metas, + _plugins, + enablePlugin, + initPlugin, + InternalPluginFlags, + preInitPlugin, + startPlugin, + type InternalPlugin, +} from '@revenge-mod/plugins/_' -import { MobileSetting } from '../constants' +import FormSwitch from '~/components/FormSwitch' +import Page from '~/components/Page' +import SearchInput from '~/components/SearchInput' + +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { Tokens } from '@revenge-mod/discord/common' +import { getAssetByName, getAssetId } from '@revenge-mod/assets' +import { useReRender } from '@revenge-mod/utils/react' + +const { Image, useWindowDimensions } = ReactNative +const { Card, Text, Stack } = Design + +export default function RevengePluginsSettingScreen() { + const [search, setSearch] = React.useState('') + + const { width } = useWindowDimensions() + const numColumns = Math.floor((width - 16) / 448) + + const plugins = React.useMemo( + () => [..._plugins.values()].map(plugin => [plugin, _metas.get(plugin.manifest.id)![2]] as const), + [_plugins], + ) + + const filteredPlugins = React.useMemo( + () => + plugins.filter(([plugin]) => { + const { name, description, author } = plugin.manifest + const query = search.toLowerCase() + return ( + name.toLowerCase().includes(query) || + description.toLowerCase().includes(query) || + author.toLowerCase().includes(query) + ) + }), + [plugins, search], + ) -export default function RevengeFontsSettingScreen() { return ( - + + setSearch(v)} /> + ( + + )} + /> + + ) +} + +const usePluginCardStyles = Design.createStyles({ + icon: { + width: 20, + height: 20, + tintColor: Tokens.default.colors.TEXT_NORMAL, + }, + card: { + flexGrow: 1, + marginBottom: 12, + paddingVertical: 12, + paddingHorizontal: 12, + gap: 4, + }, + rightGap: { + marginRight: 12, + }, + topContainer: { + alignItems: 'center', + }, + alignedContainer: { + paddingLeft: 28, + }, + grow: { + flexGrow: 1, + }, + autoSize: { + flex: 1, + }, +}) + +function InstalledPluginCard({ + plugin, + iflags, + rightGap, +}: { plugin: InternalPlugin; iflags: number; rightGap?: boolean }) { + const { + manifest: { name, description, author, icon }, + flags, + } = plugin + + const reRender = useReRender() + const essential = Boolean(iflags & InternalPluginFlags.Essential) + const enabled = Boolean(flags & PluginFlags.Enabled) + const styles = usePluginCardStyles() + + return ( + + + + + {name} + + + + + + by {author} + + + {description} + + + ) } diff --git a/src/plugins/staff-settings/index.ts b/src/plugins/staff-settings/index.ts index 960965d..7e30987 100644 --- a/src/plugins/staff-settings/index.ts +++ b/src/plugins/staff-settings/index.ts @@ -1,7 +1,7 @@ import { byProps } from '@revenge-mod/modules/finders/filters' import { waitForModules } from '@revenge-mod/modules/finders/wait' -import { after } from '@revenge-mod/patcher' +import { instead } from '@revenge-mod/patcher' import { PluginFlags } from '@revenge-mod/plugins/constants' import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' @@ -16,10 +16,18 @@ registerPlugin( }, { init({ cleanup }) { - const unsub = waitForModules(byProps('isStaffEnv'), (_, UserStoreUtils) => { + const unsub = waitForModules(byProps('isStaffEnv'), UserStoreUtils => { unsub() - cleanup(after(UserStoreUtils, 'isStaffEnv', () => true)) + cleanup(instead(UserStoreUtils, 'isStaffEnv', () => true)) }) + + cleanup(unsub) + }, + start({ plugin }) { + if (plugin.flags & PluginFlags.EnabledLate) plugin.stop() + }, + stop({ plugin }) { + plugin.flags |= PluginFlags.ReloadRequired }, }, PluginFlags.Enabled,