Delete api directory

This commit is contained in:
OTKUSteyler 2025-12-10 17:23:35 +01:00
parent ba344ac3fb
commit f8047bb853
20 changed files with 0 additions and 1596 deletions

View file

@ -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<string, Asset>;
export function* iterateAssets() {
const { flagsIndex } = getMetroCache();
const yielded = new Set<number>();
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<Asset>;
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;
}

View file

@ -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;
}

View file

@ -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));
}

View file

@ -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<CommandResult> | Promise<void>;
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;
}

View file

@ -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);
}
}
}

View file

@ -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<string, any> & { 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);
};
}

View file

@ -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";

View file

@ -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<void> {
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<string> {
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");
}

View file

@ -1 +0,0 @@
export * as fs from "./fs";

View file

@ -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<string, any> };
if (isRa1nLoader() == true) {
loader = {
name: rainLoaderIdentity.loaderName,
features: {} as Record<string, any>,
};
} else {
loader = {
name: pyonLoaderIdentity.loaderName,
features: {} as Record<string, any>,
};
}
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");
}

View file

@ -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<unknown>;
}
type ModuleProps<T extends LoaderModule> = {
name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
};
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name];
if (!module) {
throw new Error(`Module ${name} is not registered`);
}
const baseObject: Record<string, any> = {
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);
}

View file

@ -1,16 +0,0 @@
import { processColor } from "react-native";
import { getModule } from "./loader";
import type { LoaderModule } from "./loader";
interface BubbleModule extends LoaderModule {
hookBubbles(): Promise<void>;
unhookBubbles(): Promise<void>;
configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise<void>;
}
export default getModule<BubbleModule>({
name: "BubbleModule",
argumentProcessors: {
configure: args => [args[0], args[1], Number(processColor(args[2]))],
},
});

View file

@ -1,38 +0,0 @@
import { RNModules } from "./types";
const nmp = window.nativeModuleProxy;
function getNativeModule<T = any>(...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<RNModules.MMKVManager>(
"NativeCacheModule", "MMKVManager"
)!;
export const NativeFileModule = getNativeModule<RNModules.FileManager>(
"NativeFileModule", "RTNFileManager", "DCDFileManager"
)!;
export const NativeClientInfoModule = getNativeModule<RNModules.ClientInfoModule>(
"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"
)!;

View file

@ -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<unknown>;
}
type ModuleProps<T extends LoaderModule> = {
name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
};
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name];
if (!module) {
throw new Error(`Module ${name} is not registered`);
}
const baseObject: Record<string, any> = {
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);
}

View file

@ -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<string | null>;
/**
* 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<Record<string, string>>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* @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<string>;
/**
* 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<unknown>;
/**
* 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<boolean>;
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;
};
}

View file

@ -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<Callback> {
(func: string, parent: any, callback: Callback, once?: boolean): Unpatcher;
await(func: string, parent: Promise<unknown>, 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 };

View file

@ -1 +0,0 @@
export * as jsx from "./jsx";

View file

@ -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<string, Callback[]>();
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());
}

View file

@ -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<Settings>(createMMKVBackend("VENDETTA_SETTINGS")));
export const loaderConfig = wrapSync(createStorage<LoaderConfig>(
createFileBackend(getLoaderConfigPath(), {
customLoadUrl: {
enabled: false,
url: "http://localhost:4040/kettu.js"
}
})
));
export interface UpdaterSettings {
fetchPluginsOnStart?: boolean;
repoAutoFetchOverrides?: Record<string, boolean>;
}
export const updaterSettings = wrapSync(createStorage<UpdaterSettings>(createMMKVBackend("VENDETTA_UPDATER_SETTINGS")));

View file

@ -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<string, any>;
function createFileBackend<T extends object>(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<T extends object = {}>(path: string, value: T): Promise<void> {
_loadedStorage[path] = value;
createFileBackend<T>(path).set(value);
}
export function createStorageAndCallback<T extends object = {}>(
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<T>(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<T extends object> = Parameters<typeof createStorageAndCallback<T>>[2];
export async function createStorageAsync<T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): Promise<T> {
return new Promise(r => createStorageAndCallback(path, r, opts));
}
export const createStorage = <T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): 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<boolean> {
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<T extends object = {}>(path: string): T | undefined {
return _loadedStorage[path];
}