Delete api directory
This commit is contained in:
parent
ba344ac3fb
commit
f8047bb853
20 changed files with 0 additions and 1596 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
201
api/debug.ts
201
api/debug.ts
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * as fs from "./fs";
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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]))],
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
)!;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * as jsx from "./jsx";
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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")));
|
||||
|
|
@ -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];
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue