From f8047bb853e57d2eed610fb991e7f69ef45be427 Mon Sep 17 00:00:00 2001 From: OTKUSteyler <90369564+OTKUSteyler@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:23:35 +0100 Subject: [PATCH] Delete api directory --- api/assets/index.ts | 79 --------- api/assets/patches.ts | 25 --- api/commands/index.ts | 84 ---------- api/commands/types.ts | 76 --------- api/debug.ts | 201 ----------------------- api/flux/index.ts | 49 ------ api/index.ts | 9 - api/native/fs.ts | 76 --------- api/native/index.ts | 1 - api/native/loader.ts | 254 ----------------------------- api/native/modules/Loader.ts | 82 ---------- api/native/modules/bubblemodule.ts | 16 -- api/native/modules/index.ts | 38 ----- api/native/modules/loader.ts | 82 ---------- api/native/modules/types.ts | 175 -------------------- api/patcher.ts | 67 -------- api/react/index.ts | 1 - api/react/jsx.ts | 49 ------ api/settings.ts | 42 ----- api/storage/index.ts | 190 --------------------- 20 files changed, 1596 deletions(-) delete mode 100644 api/assets/index.ts delete mode 100644 api/assets/patches.ts delete mode 100644 api/commands/index.ts delete mode 100644 api/commands/types.ts delete mode 100644 api/debug.ts delete mode 100644 api/flux/index.ts delete mode 100644 api/index.ts delete mode 100644 api/native/fs.ts delete mode 100644 api/native/index.ts delete mode 100644 api/native/loader.ts delete mode 100644 api/native/modules/Loader.ts delete mode 100644 api/native/modules/bubblemodule.ts delete mode 100644 api/native/modules/index.ts delete mode 100644 api/native/modules/loader.ts delete mode 100644 api/native/modules/types.ts delete mode 100644 api/patcher.ts delete mode 100644 api/react/index.ts delete mode 100644 api/react/jsx.ts delete mode 100644 api/settings.ts delete mode 100644 api/storage/index.ts diff --git a/api/assets/index.ts b/api/assets/index.ts deleted file mode 100644 index a82ca6e..0000000 --- a/api/assets/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { getMetroCache } from "@metro/internals/caches"; -import { ModuleFlags } from "@metro/internals/enums"; -import { requireModule } from "@metro/internals/modules"; - -import { assetsModule } from "./patches"; - -export interface Asset { - id: number; - name: string; - moduleId: number; - type: string; -} - -// Cache common usage -const _nameToAssetCache = {} as Record; - -export function* iterateAssets() { - const { flagsIndex } = getMetroCache(); - const yielded = new Set(); - - for (const id in flagsIndex) { - if (flagsIndex[id] & ModuleFlags.ASSET) { - const assetId = requireModule(Number(id)); - if (typeof assetId !== "number" || yielded.has(assetId)) continue; - yield getAssetById(assetId); - yielded.add(assetId); - } - } -} - -// Apply additional properties for convenience -function getAssetById(id: number): Asset { - const asset = assetsModule.getAssetByID(id); - if (!asset) return asset; - return Object.assign(asset, { id }); -} - -/** - * Returns the first asset registry by its registry id (number), name (string) or given filter (function) - */ -export function findAsset(id: number): Asset | undefined; -export function findAsset(name: string): Asset | undefined; -export function findAsset(filter: (a: Asset) => boolean): Asset | undefined; - -export function findAsset(param: number | string | ((a: Asset) => boolean)) { - if (typeof param === "number") return getAssetById(param); - - if (typeof param === "string" && _nameToAssetCache[param]) { - return _nameToAssetCache[param]; - } - - for (const asset of iterateAssets()) { - if (typeof param === "string" && asset.name === param) { - _nameToAssetCache[param] = asset; - return asset; - } else if (typeof param === "function" && param(asset)) { - return asset; - } - } -} - -export function filterAssets(param: string | ((a: Asset) => boolean)) { - const filteredAssets = [] as Array; - - for (const asset of iterateAssets()) { - if (typeof param === "string" ? asset.name === param : param(asset)) { - filteredAssets.push(asset); - } - } - - return filteredAssets; -} - -/** - * Returns the first asset ID in the registry with the given name - */ -export function findAssetId(name: string) { - return findAsset(name)?.id; -} diff --git a/api/assets/patches.ts b/api/assets/patches.ts deleted file mode 100644 index 1a204cd..0000000 --- a/api/assets/patches.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { after } from "@lib/api/patcher"; -import { indexAssetModuleFlag } from "@metro/internals/caches"; -import { getImportingModuleId } from "@metro/internals/modules"; - -interface AssetModule { - registerAsset(assetDefinition: any): number; - getAssetByID(id: number): any; -} - -export let assetsModule: AssetModule; - -/** - * @internal - */ -export function patchAssets(module: AssetModule) { - if (assetsModule) return; - assetsModule = module; - - const unpatch = after("registerAsset", assetsModule, () => { - const moduleId = getImportingModuleId(); - if (moduleId !== -1) indexAssetModuleFlag(moduleId); - }); - - return unpatch; -} diff --git a/api/commands/index.ts b/api/commands/index.ts deleted file mode 100644 index 7a295d8..0000000 --- a/api/commands/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ApplicationCommand, ApplicationCommandInputType, ApplicationCommandType, BunnyApplicationCommand } from "@lib/api/commands/types"; -import { after, instead } from "@lib/api/patcher"; -import { logger } from "@lib/utils/logger"; -import { commands as commandsModule, messageUtil } from "@metro/common"; - -let commands: ApplicationCommand[] = []; - -/** - * @internal - */ -export function patchCommands() { - const unpatch = after("getBuiltInCommands", commandsModule, ([type], res: ApplicationCommand[]) => { - return [...res, ...commands.filter(c => - (type instanceof Array ? type.includes(c.type) : type === c.type) - && c.__bunny?.shouldHide?.() !== false) - ]; - }); - - // Register core commands - [ - require("@core/commands/eval"), - require("@core/commands/debug"), - require("@core/commands/plugins"), - require("@core/commands/reload") - ].forEach(r => registerCommand(r.default())); - - return () => { - commands = []; - unpatch(); - }; -} - -export function registerCommand(command: BunnyApplicationCommand): () => void { - // Get built in commands - let builtInCommands: ApplicationCommand[]; - try { - builtInCommands = commandsModule.getBuiltInCommands(ApplicationCommandType.CHAT, true, false); - } catch { - builtInCommands = commandsModule.getBuiltInCommands(Object.values(ApplicationCommandType), true, false); - } - - builtInCommands.sort((a: ApplicationCommand, b: ApplicationCommand) => parseInt(b.id!) - parseInt(a.id!)); - - const lastCommand = builtInCommands[builtInCommands.length - 1]; - - // Override the new command's id to the last command id - 1 - command.id = (parseInt(lastCommand.id!, 10) - 1).toString(); - - // Fill optional args - command.__bunny = { - shouldHide: command.shouldHide - }; - - command.applicationId ??= "-1"; - command.type ??= ApplicationCommandType.CHAT; - command.inputType = ApplicationCommandInputType.BUILT_IN; - command.displayName ??= command.name; - command.untranslatedName ??= command.name; - command.displayDescription ??= command.description; - command.untranslatedDescription ??= command.description; - - if (command.options) for (const opt of command.options) { - opt.displayName ??= opt.name; - opt.displayDescription ??= opt.description; - } - - instead("execute", command, (args, orig) => { - Promise.resolve( - orig.apply(command, args) - ).then(ret => { - if (ret && typeof ret === "object") { - messageUtil.sendMessage(args[1].channel.id, ret); - } - }).catch(err => { - logger.error("Failed to execute command", err); - }); - }); - - // Add it to the commands array - commands.push(command); - - // Return command id so it can be unregistered - return () => (commands = commands.filter(({ id }) => id !== command.id)); -} diff --git a/api/commands/types.ts b/api/commands/types.ts deleted file mode 100644 index c971499..0000000 --- a/api/commands/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -export interface Argument { - type: ApplicationCommandOptionType, - name: string, - value: string, - focused: undefined; - options: Argument[]; -} - -export interface ApplicationCommand { - name: string; - description: string; - execute: (args: Argument[], ctx: CommandContext) => CommandResult | void | Promise | Promise; - options: ApplicationCommandOption[]; - id?: string; - applicationId?: string; - displayName?: string; - displayDescription?: string; - untranslatedDescription?: string; - untranslatedName?: string; - inputType?: ApplicationCommandInputType; - type?: ApplicationCommandType; - __bunny?: { - shouldHide: () => boolean; - }; -} - -export interface BunnyApplicationCommand extends ApplicationCommand { - shouldHide: () => boolean; -} - -export enum ApplicationCommandInputType { - BUILT_IN, - BUILT_IN_TEXT, - BUILT_IN_INTEGRATION, - BOT, - PLACEHOLDER, -} - -export interface ApplicationCommandOption { - name: string; - description: string; - required?: boolean; - type: ApplicationCommandOptionType; - displayName?: string; - displayDescription?: string; -} - -export enum ApplicationCommandOptionType { - SUB_COMMAND = 1, - SUB_COMMAND_GROUP, - STRING, - INTEGER, - BOOLEAN, - USER, - CHANNEL, - ROLE, - MENTIONABLE, - NUMBER, - ATTACHMENT, -} - -export enum ApplicationCommandType { - CHAT = 1, - USER, - MESSAGE, -} - -export interface CommandContext { - channel: any; - guild: any; -} - -export interface CommandResult { - content: string; - tts?: boolean; -} diff --git a/api/debug.ts b/api/debug.ts deleted file mode 100644 index 870739b..0000000 --- a/api/debug.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes"; -import { findAssetId } from "@lib/api/assets"; -import { getLoaderName, getLoaderVersion, isThemeSupported, getReactDevToolsProp } from "@lib/api/native/loader"; -import { BundleUpdaterManager, NativeClientInfoModule, NativeDeviceModule } from "@lib/api/native/modules"; -import { after } from "@lib/api/patcher"; -import { settings } from "@lib/api/settings"; -import { logger } from "@lib/utils/logger"; -import { showToast } from "@ui/toasts"; -import { version } from "bunny-build-info"; -import { Platform, type PlatformConstants } from "react-native"; -import { StyleSheet } from "react-native"; - -export interface RNConstants extends PlatformConstants { - // Android - Version: number; - Release: string; - Serial: string; - Fingerprint: string; - Model: string; - Brand: string; - Manufacturer: string; - ServerHost?: string; - - // iOS - forceTouchAvailable: boolean; - interfaceIdiom: string; - osVersion: string; - systemName: string; -} - -/** - * @internal - */ -export async function toggleSafeMode() { - settings.safeMode = { ...settings.safeMode, enabled: !settings.safeMode?.enabled }; - if (isThemeSupported()) { - if (getThemeFromLoader()?.id) settings.safeMode!.currentThemeId = getThemeFromLoader()!.id; - if (settings.safeMode?.enabled) { - await selectTheme(null); - } else if (settings.safeMode?.currentThemeId) { - await selectTheme(themes[settings.safeMode?.currentThemeId]); - } - } - setTimeout(BundleUpdaterManager.reload, 400); -} - -let socket: WebSocket; -export function connectToDebugger(url: string) { - if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close(); - - if (!url) { - showToast("Invalid debugger URL!", findAssetId("Small")); - return; - } - - socket = new WebSocket(`ws://${url}`); - - socket.addEventListener("open", () => showToast("Connected to debugger.", findAssetId("Check"))); - socket.addEventListener("message", (message: any) => { - try { - (0, eval)(message.data); - } catch (e) { - console.error(e); - } - }); - - socket.addEventListener("error", (err: any) => { - console.log(`Debugger error: ${err.message}`); - showToast("An error occurred with the debugger connection!", findAssetId("Small")); - }); -} - -/** - * @internal - */ -export function patchLogHook() { - const unpatch = after("nativeLoggingHook", globalThis, args => { - if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] })); - logger.log(args[0]); - }); - - return () => { - socket && socket.close(); - unpatch(); - }; -} - -/** @internal */ -export const versionHash = version; - -export function getDebugInfo() { - // Hermes - const hermesProps = window.HermesInternal.getRuntimeProperties(); - const hermesVer = hermesProps["OSS Release Version"]; - const padding = "for RN "; - - // RN - const PlatformConstants = Platform.constants as RNConstants; - const rnVer = PlatformConstants.reactNativeVersion; - - return { - /** - * @deprecated use `bunny` field - * */ - vendetta: { - version: versionHash.split("-")[0], - loader: getLoaderName(), - }, - bunny: { - version: versionHash, - loader: { - name: getLoaderName(), - version: getLoaderVersion() - } - }, - discord: { - version: NativeClientInfoModule.getConstants().Version, - build: NativeClientInfoModule.getConstants().Build, - }, - react: { - version: React.version, - nativeVersion: hermesVer.startsWith(padding) ? hermesVer.substring(padding.length) : `${rnVer.major}.${rnVer.minor}.${rnVer.patch}`, - }, - hermes: { - version: hermesVer, - buildType: hermesProps.Build, - bytecodeVersion: hermesProps["Bytecode Version"], - }, - ...Platform.select( - { - android: { - os: { - name: "Android", - version: PlatformConstants.Release, - sdk: PlatformConstants.Version - }, - }, - ios: { - os: { - name: PlatformConstants.systemName, - version: PlatformConstants.osVersion - }, - } - } - )!, - ...Platform.select( - { - android: { - device: { - manufacturer: PlatformConstants.Manufacturer, - brand: PlatformConstants.Brand, - model: PlatformConstants.Model, - codename: NativeDeviceModule.device - } - }, - ios: { - device: { - manufacturer: NativeDeviceModule.deviceManufacturer, - brand: NativeDeviceModule.deviceBrand, - model: NativeDeviceModule.deviceModel, - codename: NativeDeviceModule.device - } - } - } - )! - }; -} - - -/** - * @internal - */ -export function initDebugger() { - if (settings.autoDebugger) { - try { - connectToDebugger(settings.debuggerUrl); - } catch (e) { - logger.error("Failed to connect to Debugger during startup:", e); - } - } - if (settings.autoDevTools) { - try { - (async () => { - const devTools = window[getReactDevToolsProp() || "__vendetta_rdc"]; - - if (!devTools?.connectToDevTools) { - showToast("Invalid devTools URL!", findAssetId("Small")); - return; - } - - await devTools.connectToDevTools({ - host: settings.devToolsUrl.split(":")?.[0], - resolveRNStyle: StyleSheet.flatten, - }); - })(); - } catch (e) { - logger.error("Failed to connect to ReactDevTools during startup:", e); - } - } - -} diff --git a/api/flux/index.ts b/api/flux/index.ts deleted file mode 100644 index 880902c..0000000 --- a/api/flux/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// shelter-mod inspired -import { FluxDispatcher } from "@metro/common"; - -const blockedSym = Symbol.for("bunny.flux.blocked"); -const modifiedSym = Symbol.for("bunny.flux.modified"); - -export const dispatcher = FluxDispatcher; - -type Intercept = (payload: Record & { type: string; }) => any; -let intercepts: Intercept[] = []; - -/** - * @internal - */ -export function injectFluxInterceptor() { - const cb = (payload: any) => { - for (const intercept of intercepts) { - const res = intercept(payload); - - // nullish -> nothing, falsy -> block, object -> modify - if (res == null) { - continue; - } else if (!res) { - payload[blockedSym] = true; - } else if (typeof res === "object") { - Object.assign(payload, res); - payload[modifiedSym] = true; - } - } - - return blockedSym in payload; - }; - - (dispatcher._interceptors ??= []).unshift(cb); - - return () => dispatcher._interceptors &&= dispatcher._interceptors.filter(v => v !== cb); -} - -/** - * Intercept Flux dispatches. Return type affects the end result, where - * nullish -> nothing, falsy -> block, object -> modify - */ -export function intercept(cb: Intercept) { - intercepts.push(cb); - - return () => { - intercepts = intercepts.filter(i => i !== cb); - }; -} diff --git a/api/index.ts b/api/index.ts deleted file mode 100644 index 4d9057a..0000000 --- a/api/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * as assets from "./assets"; -export * as commands from "./commands"; -export * as debug from "./debug"; -export * as flux from "./flux"; -export * as native from "./native"; -export * as patcher from "./patcher"; -export * as react from "./react"; -export * as settings from "./settings"; -export * as storage from "./storage"; diff --git a/api/native/fs.ts b/api/native/fs.ts deleted file mode 100644 index 131dbfa..0000000 --- a/api/native/fs.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { NativeFileModule } from "./modules"; - -/** - * Removes all files in a directory from the path given - * @param path Path to the targeted directory - */ -export async function clearFolder(path: string, { prefix = "pyoncord/" } = {}) { - if (typeof NativeFileModule.clearFolder !== "function") throw new Error("'fs.clearFolder' is not supported"); - return void await NativeFileModule.clearFolder("documents", `${prefix}${path}`); -} - -/** - * Remove file from given path, currently no check for any failure - * @param path Path to the file - */ -export async function removeFile(path: string, { prefix = "pyoncord/" } = {}) { - if (typeof NativeFileModule.removeFile !== "function") throw new Error("'fs.removeFile' is not supported"); - return void await NativeFileModule.removeFile("documents", `${prefix}${path}`); -} - -/** - * Remove file from given path, currently no check for any failure - * @param path Path to the file - */ -export async function removeCacheFile(path: string, prefix = "pyoncord/") { - if (typeof NativeFileModule.removeFile !== "function") throw new Error("'fs.removeFile' is not supported"); - return void await NativeFileModule.removeFile("cache", `${prefix}${path}`); -} - -/** - * Check if the file or directory given by the path exists - * @param path Path to the file - */ -export async function fileExists(path: string, { prefix = "pyoncord/" } = {}) { - return await NativeFileModule.fileExists(`${NativeFileModule.getConstants().DocumentsDirPath}/${prefix}${path}`); -} - -/** - * A wrapper to write to a file to the documents directory - * @param path Path to the file - * @param data String data to write to the file - */ -export async function writeFile(path: string, data: string, { prefix = "pyoncord/" } = {}): Promise { - if (typeof data !== "string") throw new Error("Argument 'data' must be a string"); - return void await NativeFileModule.writeFile("documents", `${prefix}${path}`, data, "utf8"); -} - -/** - * A wrapper to read a file from the documents directory - * @param path Path to the file - * @param fallback Fallback data to return if the file doesn't exist, and will be written to the file - */ -export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Promise { - try { - return await NativeFileModule.readFile(`${NativeFileModule.getConstants().DocumentsDirPath}/${prefix}${path}`, "utf8"); - } catch (err) { - throw new Error(`An error occured while writing to '${path}'`, { cause: err }); - } -} - -/** - * Download a file from the given URL and save it to the path given - * @param url URL to download the file from - * @param path Path to save the file to - */ -export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to download file from ${url}: ${response.status}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const data = Buffer.from(arrayBuffer).toString("base64"); - - await NativeFileModule.writeFile("documents", `${prefix}${path}`, data, "base64"); -} diff --git a/api/native/index.ts b/api/native/index.ts deleted file mode 100644 index 997738e..0000000 --- a/api/native/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as fs from "./fs"; diff --git a/api/native/loader.ts b/api/native/loader.ts deleted file mode 100644 index e38b405..0000000 --- a/api/native/loader.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { VdThemeInfo } from "@lib/addons/themes"; -import { removeCacheFile } from "./fs"; - -// @ts-ignore -const pyonLoaderIdentity = globalThis.__PYON_LOADER__; - -// @ts-ignore -const rainLoaderIdentity = globalThis.__RAIN_LOADER__; - -// @ts-ignore -const vendettaLoaderIdentity = globalThis.__vendetta_loader; - -export interface VendettaLoaderIdentity { - name: string; - features: { - loaderConfig?: boolean; - devtools?: { - prop: string; - version: string; - }, - themes?: { - prop: string; - }; - }; -} - -export function isVendettaLoader() { - return vendettaLoaderIdentity != null; -} - -export function isPyonLoader() { - return pyonLoaderIdentity != null; -} - -export function isRa1nLoader() { - return rainLoaderIdentity != null; -} - -function polyfillVendettaLoaderIdentity() { - if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null; - - let loader: { name: string; features: Record }; - - if (isRa1nLoader() == true) { - loader = { - name: rainLoaderIdentity.loaderName, - features: {} as Record, - }; - } else { - loader = { - name: pyonLoaderIdentity.loaderName, - features: {} as Record, - }; - } - - if (isLoaderConfigSupported()) loader.features.loaderConfig = true; - if (isSysColorsSupported()) { - loader.features.syscolors = { - prop: "__vendetta_syscolors" - }; - - Object.defineProperty(globalThis, "__vendetta_syscolors", { - get: () => getSysColors(), - configurable: true - }); - } - if (isThemeSupported()) { - loader.features.themes = { - prop: "__vendetta_theme" - }; - - Object.defineProperty(globalThis, "__vendetta_theme", { - // get: () => getStoredTheme(), - get: () => { - // PyonXposed only returns keys it parses, making custom keys like Themes+' to gone - const id = getStoredTheme()?.id; - if (!id) return null; - - const { themes } = require("@lib/addons/themes"); - return themes[id] ?? getStoredTheme() ?? null; - }, - configurable: true - }); - } - - Object.defineProperty(globalThis, "__vendetta_loader", { - get: () => loader, - configurable: true - }); - - return loader as VendettaLoaderIdentity; -} - -export function getLoaderIdentity() { - if (isPyonLoader()) { - return pyonLoaderIdentity; - } else if (isVendettaLoader()) { - return getVendettaLoaderIdentity(); - } else if (isRa1nLoader()) { - return rainLoaderIdentity(); - } - - return null; -} - -export function getVendettaLoaderIdentity(): VendettaLoaderIdentity | null { - // @ts-ignore - if (globalThis.__vendetta_loader) return globalThis.__vendetta_loader; - return polyfillVendettaLoaderIdentity(); -} - -// add to __vendetta_loader anyway -getVendettaLoaderIdentity(); - -export function getLoaderName() { - if (isPyonLoader()) return pyonLoaderIdentity.loaderName; - else if (isRa1nLoader()) return rainLoaderIdentity.loadername; - else if (isVendettaLoader()) return vendettaLoaderIdentity.name; - - return "Unknown"; -} - -export function getLoaderVersion(): string | null { - if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion; - else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion; - return null; -} - -export function isLoaderConfigSupported() { - if (isPyonLoader()) { - return true; - } else if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.loaderConfig; - } else if (isRa1nLoader()) { - return true; - } - - return false; -} - -export function isThemeSupported() { - if (isPyonLoader()) { - return pyonLoaderIdentity.hasThemeSupport; - } else if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.themes != null; - } else if (isRa1nLoader()) { - return false; // Ra1n has theme support disabled, this is here just to make sure it doesnt think it does - } - - return false; -} - -export function getStoredTheme(): VdThemeInfo | null { - if (isPyonLoader()) { - return pyonLoaderIdentity.storedTheme; - } else if (isVendettaLoader()) { - const themeProp = vendettaLoaderIdentity!!.features.themes?.prop; - if (!themeProp) return null; - // @ts-ignore - return globalThis[themeProp] || null; - } - - return null; -} - -export function getThemeFilePath() { - if (isPyonLoader()) { - return "pyoncord/current-theme.json"; - } else if (isVendettaLoader()) { - return "vendetta_theme.json"; - } - - return null; -} - -export function isReactDevToolsPreloaded() { - if (isPyonLoader()) { - return Boolean(window.__reactDevTools); - } - if (isRa1nLoader()) { - return false; - } - if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.devtools != null; - } - - return false; -} - -export function getReactDevToolsProp(): string | null { - if (!isReactDevToolsPreloaded()) return null; - - if (isPyonLoader()) { - window.__pyoncord_rdt = window.__reactDevTools.exports; - return "__pyoncord_rdt"; - } - - if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.devtools!!.prop; - } - - return null; -} - -export function getReactDevToolsVersion() { - if (!isReactDevToolsPreloaded()) return null; - - if (isPyonLoader()) { - return window.__reactDevTools.version || null; - } - if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.devtools!!.version; - } - - return null; -} - -export function isSysColorsSupported() { - return true; -} - -export function getSysColors() { - if (!isSysColorsSupported()) return null; - if (isPyonLoader()) { - return pyonLoaderIdentity.sysColors; - } else if (isVendettaLoader()) { - return vendettaLoaderIdentity!!.features.syscolors!!.prop; - } - - return null; -} - -export function getLoaderConfigPath() { - if (isPyonLoader()) { - return "pyoncord/loader.json"; - } else if (isVendettaLoader()) { - return "vendetta_loader.json"; - } else if (isRa1nLoader()) { - return "rain/loader.json"; - } - - return "loader.json"; -} - -export function isFontSupported() { - if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2; - - return false; -} - -export async function clearBundle() { - // TODO: This should be not be hardcoded, maybe put in loader.json? - return void await removeCacheFile("bundle.js"); -} \ No newline at end of file diff --git a/api/native/modules/Loader.ts b/api/native/modules/Loader.ts deleted file mode 100644 index 57e4284..0000000 --- a/api/native/modules/Loader.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ImageLoader } from "./"; -import type { LoaderPayload } from "./types"; - -type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; - -export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; - -export function isModuleRegistered(module: string) { - return loaderPayload.loader.modules[module] !== undefined; -} - -export function isFunctionRegistered(module: string, function_: string) { - return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; -} - -export async function callFunction(module: string, functionName: string, args: unknown[]) { - if (!isFunctionRegistered(module, functionName)) { - throw new Error(`Function ${module}.${functionName} is not registered`); - } - - const promise: InteropReturn = ImageLoader.queryCache([ - "__pyon_bridge", - JSON.stringify({ m: module, f: functionName, a: args }), - ]); - - const result = await promise; - - if ("ret" in result) { - return JSON.parse(result.ret); - } - - if ("cancelled" in result) { - throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); - } - - if ("err" in result) { - throw new Error(result.err); - } -} - -export interface LoaderModule { - hasFunction(functionName: string): boolean; - callFunction(functionName: string, args: unknown[]): Promise; -} - -type ModuleProps = { - name: string; - argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; -}; - -export function getModule({ name, argumentProcessors: processArgs }: ModuleProps) { - const module = loaderPayload.loader.modules[name]; - if (!module) { - throw new Error(`Module ${name} is not registered`); - } - - const baseObject: Record = { - hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), - callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), - }; - - for (const [functionName] of Object.entries(module.functions)) { - baseObject[functionName] = (...args: unknown[]) => - callFunction( - name, - functionName, - // @ts-expect-error - processArgs && functionName in processArgs ? processArgs[functionName](args) : args, - ); - } - - return baseObject as T; -} - -/** - * Determines if safe mode is currently enabled. The result remains consistent over time. - * This will not change until the app is restarted. For draft changes, use the InitConfigStore. - * @returns True if safe mode is enabled for the current instance, otherwise false. - */ -export function isSafeModeEnabled() { - return Boolean(loaderPayload.loader.initConfig.safeMode); -} diff --git a/api/native/modules/bubblemodule.ts b/api/native/modules/bubblemodule.ts deleted file mode 100644 index c6e3d85..0000000 --- a/api/native/modules/bubblemodule.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { processColor } from "react-native"; -import { getModule } from "./loader"; -import type { LoaderModule } from "./loader"; - -interface BubbleModule extends LoaderModule { - hookBubbles(): Promise; - unhookBubbles(): Promise; - configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise; -} - -export default getModule({ - name: "BubbleModule", - argumentProcessors: { - configure: args => [args[0], args[1], Number(processColor(args[2]))], - }, -}); diff --git a/api/native/modules/index.ts b/api/native/modules/index.ts deleted file mode 100644 index a8d8f65..0000000 --- a/api/native/modules/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { RNModules } from "./types"; - -const nmp = window.nativeModuleProxy; - -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; - } - - 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" -)!; -export const ImageLoader = getNativeModule( - "ImageLoader" -)!; diff --git a/api/native/modules/loader.ts b/api/native/modules/loader.ts deleted file mode 100644 index 7a6fd65..0000000 --- a/api/native/modules/loader.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ImageLoader } from "./"; -import type { LoaderPayload } from "./types"; - -type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; - -export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; - -export function isModuleRegistered(module: string) { - return loaderPayload.loader.modules[module] !== undefined; -} - -export function isFunctionRegistered(module: string, function_: string) { - return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; -} - -export async function callFunction(module: string, functionName: string, args: unknown[]) { - if (!isFunctionRegistered(module, functionName)) { - throw new Error(`Function ${module}.${functionName} is not registered`); - } - - const promise: InteropReturn = ImageLoader.queryCache([ - "__pyon_bridge", - JSON.stringify({ m: module, f: functionName, a: args }), - ]); - - const result = await promise; - - if ("ret" in result) { - return JSON.parse(result.ret); - } - - if ("cancelled" in result) { - throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); - } - - if ("err" in result) { - throw new Error(result.err); - } -} - -export interface LoaderModule { - hasFunction(functionName: string): boolean; - callFunction(functionName: string, args: unknown[]): Promise; -} - -type ModuleProps = { - name: string; - argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; -}; - -export function getModule({ name, argumentProcessors: processArgs }: ModuleProps) { - const module = loaderPayload.loader.modules[name]; - if (!module) { - throw new Error(`Module ${name} is not registered`); - } - - const baseObject: Record = { - hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), - callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), - }; - - for (const [functionName] of Object.entries(module.functions)) { - baseObject[functionName] = (...args: unknown[]) => - callFunction( - name, - functionName, - // @ts-expect-error - processArgs && functionName in processArgs ? processArgs[functionName](args) : args, - ); - } - - return baseObject as T; -} - -/** - * Determines if safe mode is currently enabled. The result remains consistent over time. - * This will not change until the app is restarted. For draft changes, use the InitConfigStore. - * @returns True if safe mode is enabled for the current instance, otherwise false. - */ -export function isSafeModeEnabled() { - return Boolean(loaderPayload.loader.initConfig.safeMode); -} \ No newline at end of file diff --git a/api/native/modules/types.ts b/api/native/modules/types.ts deleted file mode 100644 index a20d02e..0000000 --- a/api/native/modules/types.ts +++ /dev/null @@ -1,175 +0,0 @@ -export namespace RNModules { - /** - * A key-value storage based upon `SharedPreferences` on Android. - * - * These types are based on Android though everything should be the same between - * platforms. - */ - export interface MMKVManager { - /** - * Get the value for the given `key`, or null - * @param key The key to fetch - */ - getItem: (key: string) => Promise; - /** - * Deletes the value for the given `key` - * @param key The key to delete - */ - removeItem: (key: string) => void; - /** - * Sets the value of `key` to `value` - */ - setItem: (key: string, value: string) => void; - /** - * Goes through every item in storage and returns it, excluding the - * keys specified in `exclude`. - * @param exclude A list of items to exclude from result - */ - refresh: (exclude: string[]) => Promise>; - /** - * You will be murdered if you use this function. - * Clears ALL of Discord's settings. - */ - clear: () => void; - } - - export interface FileManager { - /** - * @param path **Full** path to file - */ - fileExists: (path: string) => Promise; - /** - * Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3)) - */ - getSize: (uri: string) => Promise; - /** - * @param path **Full** path to file - * @param encoding Set to `base64` in order to encode response - */ - readFile(path: string, encoding: "base64" | "utf8"): Promise; - saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise; - /** - * @param storageDir Either `cache` or `documents`. - * @param path Path in `storageDir`, parents are recursively created. - * @param data The data to write to the file - * @param encoding Set to `base64` if `data` is base64 encoded. - * @returns Promise that resolves to path of the file once it got written - */ - writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise; - /** - * Removes a file from the path given. - * (!) On Android, this always returns false, regardless if it fails or not! - * @param storageDir Either `cache` or `documents` - * @param path Path to the file to be removed - */ - removeFile(storageDir: "cache" | "documents", path: string): Promise; - /** - * Clear the folder from the path given - * (!) On Android, this only clears all *files* and not subdirectories! - * @param storageDir Either `cache` or `documents` - * @param path Path to the folder to be cleared - * @returns Whether the clearance succeeded - */ - clearFolder(storageDir: "cache" | "documents", path: string): Promise; - getConstants: () => { - /** - * The path the `documents` storage dir (see {@link writeFile}) represents. - */ - DocumentsDirPath: string; - CacheDirPath: string; - }; - /** - * Will apparently cease to exist some time in the future so please use {@link getConstants} instead. - * @deprecated - */ - 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 - } - } -} - -export interface InitConfig { - baseUrl: string; - forceUpdate: boolean; - skipUpdate: boolean; - bundlePath: string | null; - safeMode: boolean; -} - -interface LoaderConstants { - WINTRY_DIR: string; - DEFAULT_BASE_URL: string; -} - -interface LoaderModule { - functions: { - [functionName: string]: number; - }; - constants: { - [constantName: string]: any; - }; -} - -interface LoaderModules { - [moduleName: string]: LoaderModule; -} - -export interface LoaderPayload { - bundle: { - revision: string; - }; - loader: { - name: string; - version: string; - initConfig: InitConfig; - constants: LoaderConstants; - modules: LoaderModules; - }; -} diff --git a/api/patcher.ts b/api/patcher.ts deleted file mode 100644 index 098c831..0000000 --- a/api/patcher.ts +++ /dev/null @@ -1,67 +0,0 @@ -const { - after: _after, - before: _before, - instead: _instead -} = require("spitroast"); - -/** @internal */ -export const _patcherDelaySymbol = Symbol.for("bunny.api.patcher.delay"); - -type Unpatcher = () => boolean; -type DelayCallback = (callback: (target: any) => void) => unknown; -type Thenable = { then: typeof Promise.prototype.then }; - -interface PatchFn { - (func: string, parent: any, callback: Callback, once?: boolean): Unpatcher; - await(func: string, parent: Promise, callback: Callback, once?: boolean): Unpatcher; -} - -type BeforeFn = PatchFn<(args: any[]) => unknown | unknown[]>; -type InsteadFn = PatchFn<(args: any[], origFunc: Function) => unknown>; -type AfterFn = PatchFn<(args: any[], ret: any) => unknown>; - -function create(fn: Function) { - function patchFn(this: any, ...args: any[]) { - if (typeof args[1][_patcherDelaySymbol] === "function") { - const delayCallback: DelayCallback = args[1][_patcherDelaySymbol]; - - let cancel = false; - let unpatch = () => cancel = true; - - delayCallback(target => { - if (cancel) return; - args[1] = target; - unpatch = fn.apply(this, args); - }); - - return () => unpatch(); - } - - return fn.apply(this, args); - } - - function promisePatchFn(this: any, ...args: [any, Thenable, ...any]) { - const thenable = args[1]; - if (!thenable || !("then" in thenable)) throw new Error("target is not a then-able object"); - - let cancel = false; - let unpatch = () => cancel = true; - - thenable.then(target => { - if (cancel) return; - args[1] = target; - unpatch = patchFn.apply(this, args); - }); - - return () => unpatch(); - } - - return Object.assign(patchFn, { await: promisePatchFn }); -} - -export const after = create(_after) as AfterFn; -export const before = create(_before) as BeforeFn; -export const instead = create(_instead) as InsteadFn; - -/** @internal */ -export default { after, before, instead }; diff --git a/api/react/index.ts b/api/react/index.ts deleted file mode 100644 index 03149cd..0000000 --- a/api/react/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as jsx from "./jsx"; diff --git a/api/react/jsx.ts b/api/react/jsx.ts deleted file mode 100644 index b3d025b..0000000 --- a/api/react/jsx.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { after } from "@lib/api/patcher"; -import { findByPropsLazy } from "@metro"; - -type Callback = (Component: any, ret: JSX.Element) => JSX.Element; -const callbacks = new Map(); - -const jsxRuntime = findByPropsLazy("jsx", "jsxs"); - -export function onJsxCreate(Component: string, callback: Callback) { - if (!callbacks.has(Component)) callbacks.set(Component, []); - callbacks.get(Component)!.push(callback); -} - -export function deleteJsxCreate(Component: string, callback: Callback) { - if (!callbacks.has(Component)) return; - const cbs = callbacks.get(Component)!; - cbs.splice(cbs.indexOf(callback), 1); - if (cbs.length === 0) callbacks.delete(Component); -} - -/** - * @internal - */ -export function patchJsx() { - const callback = ([Component]: unknown[], ret: JSX.Element) => { - // Band-aid fix for iOS invalid element type crashes - if (typeof ret.type === 'undefined') { - ret.type = 'RCTView' - return ret - } - - // The check could be more complex, but this is fine for now to avoid overhead - if (typeof Component === "function" && callbacks.has(Component.name)) { - const cbs = callbacks.get(Component.name)!; - for (const cb of cbs) { - const _ret = cb(Component, ret); - if (_ret !== undefined) ret = _ret; - } - return ret; - } - }; - - const patches = [ - after("jsx", jsxRuntime, callback), - after("jsxs", jsxRuntime, callback) - ]; - - return () => patches.forEach(unpatch => unpatch()); -} diff --git a/api/settings.ts b/api/settings.ts deleted file mode 100644 index 05f1c54..0000000 --- a/api/settings.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage"; -import { getLoaderConfigPath } from "@lib/api/native/loader"; - -export interface Settings { - debuggerUrl: string; - devToolsUrl: string; - autoDebugger: boolean; - autoDevTools: boolean; - developerSettings: boolean; - enableDiscordDeveloperSettings: boolean; - safeMode?: { - enabled: boolean; - currentThemeId?: string; - }; - enableEvalCommand?: boolean; -} - -export interface LoaderConfig { - customLoadUrl: { - enabled: boolean; - url: string; - }; - loadReactDevTools: boolean; -} - -export const settings = wrapSync(createStorage(createMMKVBackend("VENDETTA_SETTINGS"))); - -export const loaderConfig = wrapSync(createStorage( - createFileBackend(getLoaderConfigPath(), { - customLoadUrl: { - enabled: false, - url: "http://localhost:4040/kettu.js" - } - }) -)); - -export interface UpdaterSettings { - fetchPluginsOnStart?: boolean; - repoAutoFetchOverrides?: Record; -} - -export const updaterSettings = wrapSync(createStorage(createMMKVBackend("VENDETTA_UPDATER_SETTINGS"))); diff --git a/api/storage/index.ts b/api/storage/index.ts deleted file mode 100644 index 5746429..0000000 --- a/api/storage/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Emitter } from "@core/vendetta/Emitter"; -import { Observable, ObserverOptions } from "@gullerya/object-observer"; -import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs"; -import { debounce } from "es-toolkit"; - -const storageInitErrorSymbol = Symbol.for("bunny.storage.initError"); -const storagePromiseSymbol = Symbol.for("bunny.storage.promise"); - -const _loadedStorage = {} as Record; - -function createFileBackend(filePath: string) { - const write = debounce((data: T) => { - writeFile(filePath, JSON.stringify(data)); - }, 500); - - return { - get: async () => { - try { - return JSON.parse(await readFile(filePath)) as T; - } catch (e) { - throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e }); - } - }, - set: async (data: T) => { - if (!data || typeof data !== "object") { - throw new Error("data needs to be an object"); - } - - write(data); - }, - exists: async () => { - return await fileExists(filePath); - } - }; -} - -export function useObservable(observables: Observable[], opts?: ObserverOptions) { - if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error( - "An error occured while initializing the storage", - ); - - if (observables.some(o => !Observable.isObservable(o))) { - throw new Error("Argument passed isn't an Observable"); - } - - const [, forceUpdate] = React.useReducer(n => ~n, 0); - - React.useEffect(() => { - const listener = () => forceUpdate(); - - observables.forEach(o => Observable.observe(o, listener, opts)); - - return () => { - observables.forEach(o => Observable.unobserve(o, listener)); - }; - }, []); -} - -export async function updateStorage(path: string, value: T): Promise { - _loadedStorage[path] = value; - createFileBackend(path).set(value); -} - -export function createStorageAndCallback( - path: string, - cb: (proxy: T) => void, - { - dflt = {} as T, - nullIfEmpty = false - } = {} -) { - let emitter: Emitter; - - const callback = (data: any) => { - const proxy = new Proxy(Observable.from(data), { - get(target, prop, receiver) { - if (prop === Symbol.for("vendetta.storage.emitter")) { - if (emitter) return emitter; - emitter = new Emitter(); - - Observable.observe(target, changes => { - for (const change of changes) { - emitter.emit(change.type !== "delete" ? "SET" : "DEL", { - path: change.path, - value: change.value - }); - } - }); - - return emitter; - } - - return Reflect.get(target, prop, receiver); - }, - }); - - const handler = () => backend.set(proxy); - Observable.observe(proxy, handler); - - cb(proxy); - }; - - const backend = createFileBackend(path); - if (_loadedStorage[path]) { - callback(_loadedStorage[path]); - } else { - backend.exists().then(async exists => { - if (!exists) { - if (nullIfEmpty) { - callback(_loadedStorage[path] = null); - } else { - _loadedStorage[path] = dflt; - await backend.set(dflt); - callback(dflt); - } - } else { - callback(_loadedStorage[path] = await backend.get()); - } - }); - } -} - -type StorageOptions = Parameters>[2]; - -export async function createStorageAsync( - path: string, - opts: StorageOptions = {} -): Promise { - return new Promise(r => createStorageAndCallback(path, r, opts)); -} - -export const createStorage = ( - path: string, - opts: StorageOptions = {} -): T => { - const promise = new Promise(r => resolvePromise = r); - let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void; - - createStorageAndCallback(path, proxy => { - awaited = proxy; - resolved = true; - resolvePromise(); - }, opts); - - const check = () => { - if (resolved) return true; - throw new Error(`Attempted to access storage without initializing: ${path}`); - }; - - return new Proxy({} as any, { - ...Object.fromEntries( - Object.getOwnPropertyNames(Reflect) - .map(k => [k, (t: T, ...a: any[]) => { - // @ts-expect-error - return check() && Reflect[k](awaited, ...a); - }]) - ), - get(target, prop, recv) { - if (prop === storageInitErrorSymbol) return error; - if (prop === storagePromiseSymbol) return promise; - return check() && Reflect.get(awaited ?? target, prop, recv); - }, - }); -}; - -export async function preloadStorageIfExists(path: string): Promise { - if (_loadedStorage[path]) return true; - - const backend = createFileBackend(path); - if (await backend.exists()) { - _loadedStorage[path] = await backend.get(); - return true; - } - - return false; -} - -export async function purgeStorage(path: string) { - await removeFile(path); - delete _loadedStorage[path]; -} - -export function awaitStorage(...proxies: any[]) { - return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol])); -} - -/** @internal */ -export function getPreloadedStorage(path: string): T | undefined { - return _loadedStorage[path]; -}