revenge-plugins/plugins/server-drawer/src/patches/fakeQuestDock.ts

180 lines
7.8 KiB
TypeScript

import { findByProps } from "@vendetta/metro";
import { rawFindByFunctionProps, rawFindByStoreName } from "../lib/rawFind";
// Confirmed live: rawFindByFunctionProps never found any of these four on some accounts, across
// multiple reload and account-switch cycles spanning over a minute - not a timing race, since
// rawFind only scans modules Metro has *already* naturally initialized and never triggers one
// itself. On an account where nothing in the normal boot path happens to touch this specific
// module, passive scanning can retry forever and never find it. Revenge's own findByProps forces
// a module to initialize if it isn't yet, which is exactly what's needed here - confirmed live via
// direct eval that it finds this exact module instantly on an account where rawFind never did.
// Tried first since it's free and carries no caching risk; findByProps is the fallback precisely
// because a failed call there is cached negatively forever, so it's only worth risking once
// rawFind has already had a real chance to succeed on its own.
function findHookModule(...props: string[]): any {
return rawFindByFunctionProps<any>(...props) ?? findByProps(...props);
}
const TAG = "[ServerDrawer]";
// A real, always-available Discord CDN URL - confirmed live that an empty string still resolves
// "successfully" through Discord's buildUrl (concatenates onto the CDN base rather than being
// recognized as absolute) but produces a malformed URL, and a full external https:// URL gets
// double-prefixed the same way ("https://cdn.discordapp.com/https://..."). Neither crashes -
// getQuestAsset below is guarded regardless - so this is cosmetic, not load-bearing.
const FALLBACK_ASSET_URL = "https://cdn.discordapp.com/embed/avatars/0.png";
function buildFallbackQuest(userId: string | undefined) {
const now = new Date();
const nowIso = now.toISOString();
const farFuture = new Date(now.getTime() + 1000 * 60 * 60 * 24 * 365 * 5).toISOString();
return {
id: "server-drawer",
preview: false,
config: {
id: "server-drawer",
configVersion: 2,
startsAt: nowIso,
expiresAt: farFuture,
features: [],
assets: {
hero: FALLBACK_ASSET_URL, heroVideo: "", questBarHero: FALLBACK_ASSET_URL, questBarHeroBlurhash: "",
questBarHeroVideo: "", gameTile: FALLBACK_ASSET_URL, logotype: FALLBACK_ASSET_URL,
gameTileLight: FALLBACK_ASSET_URL, gameTileDark: FALLBACK_ASSET_URL,
logotypeLight: FALLBACK_ASSET_URL, logotypeDark: FALLBACK_ASSET_URL,
},
colors: { primary: "#5865F2", secondary: "#000000" },
messages: {
questName: "Server Drawer",
gameTitle: "All quests complete!",
gamePublisher: "ServerDrawer plugin",
},
taskConfigV2: {
tasks: {
ACHIEVEMENT_IN_ACTIVITY: {
type: "ACHIEVEMENT_IN_ACTIVITY",
target: 1,
eventName: "progress",
messages: {
taskTitle: "Server Drawer is active",
taskDescription: "You've got no active Discord quests right now - enjoy the drawer!",
},
applications: [{ id: "0" }],
},
},
joinOperator: "or",
},
rewardsConfig: {
assignmentMethod: 1,
rewards: [{
type: 4,
skuId: "0",
messages: {
redemptionInstructionsByPlatform: {},
name: "Nothing - just vibes",
nameWithArticle: "Nothing - just vibes",
},
orbQuantity: 0,
premiumOrbQuantity: 0,
}],
rewardsExpireAt: farFuture,
platforms: [0],
},
cosponsorMetadata: undefined,
sharePolicy: "shareable_everywhere",
ctaConfig: { android: undefined, ios: undefined, link: "", buttonLabel: "", subtitle: "" },
},
userStatus: {
userId: userId ?? null,
questId: "server-drawer",
enrolledAt: nowIso,
completedAt: null,
claimedAt: null,
claimedTier: null,
orbQuantityClaimed: 0,
lastStreamHeartbeatAt: null,
streamProgressSeconds: 0,
dismissedQuestContent: 0,
progress: {},
},
targetedContent: [],
trafficMetadataSealed: "",
};
}
// Each of these four does its own completely independent rawFind lookup rather than sharing one
// resolved module reference across all of them - confirmed live (GuildsBar's own patch writeup)
// that a metro search can land on a different module copy than another search for a related
// property, even when both "succeed", so patching useMobileQuestDock and
// useIsMobileQuestDockRendered off the same found object was never guaranteed to be the same
// reference the real render path actually reads from. Matches the original plugin's four separate
// files (questDockRender.ts, questDockBase.ts, mobileQuestDock.ts, getQuestAsset.ts) in spirit,
// just retry-safe.
export function patchMobileQuestDock(cleanups: (() => void)[]): boolean {
const mod = findHookModule("useMobileQuestDock");
if (!mod?.useMobileQuestDock) {
console.log(TAG, "WARN: useMobileQuestDock not found yet (will retry)");
return false;
}
const orig = mod.useMobileQuestDock;
let cachedUserId: string | undefined;
mod.useMobileQuestDock = function (this: unknown, ...args: any[]) {
const real = orig.apply(this, args);
if (real) return real;
if (cachedUserId === undefined) {
cachedUserId = rawFindByStoreName<any>("UserStore")?.getCurrentUser?.()?.id;
}
return buildFallbackQuest(cachedUserId);
};
cleanups.push(() => { mod.useMobileQuestDock = orig; });
return true;
}
export function patchQuestDockRender(cleanups: (() => void)[]): boolean {
const mod = findHookModule("useIsMobileQuestDockRendered");
if (!mod?.useIsMobileQuestDockRendered) {
console.log(TAG, "WARN: useIsMobileQuestDockRendered not found yet (will retry)");
return false;
}
const orig = mod.useIsMobileQuestDockRendered;
mod.useIsMobileQuestDockRendered = function (this: unknown, ...args: any[]) {
orig.apply(this, args);
return true;
};
cleanups.push(() => { mod.useIsMobileQuestDockRendered = orig; });
return true;
}
export function patchQuestDockBase(cleanups: (() => void)[]): boolean {
const mod = findHookModule("useIsMobileQuestDockRenderedBase");
if (!mod?.useIsMobileQuestDockRenderedBase) {
console.log(TAG, "WARN: useIsMobileQuestDockRenderedBase not found yet (will retry)");
return false;
}
const orig = mod.useIsMobileQuestDockRenderedBase;
mod.useIsMobileQuestDockRenderedBase = function (this: unknown, ...args: any[]) {
orig.apply(this, args);
return true;
};
cleanups.push(() => { mod.useIsMobileQuestDockRenderedBase = orig; });
return true;
}
export function patchGetQuestAsset(cleanups: (() => void)[]): boolean {
const mod = findHookModule("getQuestAsset");
if (!mod?.getQuestAsset) {
console.log(TAG, "WARN: getQuestAsset not found yet (will retry)");
return false;
}
const orig = mod.getQuestAsset;
mod.getQuestAsset = function (this: unknown, ...args: any[]) {
try {
return orig.apply(this, args);
} catch {
return { url: null, isAnimated: false };
}
};
cleanups.push(() => { mod.getQuestAsset = orig; });
return true;
}