Add auto-update + fail-closed block gate to every plugin
This commit is contained in:
parent
a48b49f54f
commit
1a47431391
25 changed files with 456 additions and 122 deletions
110
docs/shared-modules-audit.md
Normal file
110
docs/shared-modules-audit.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Shared modules audit (classic revenge-plugins)
|
||||
|
||||
Date: 2026-08-16
|
||||
Scope: `/root/revenge-plugins` only (Next repo not covered). Audit only — no code changed.
|
||||
|
||||
Goal: (1) give a reference sheet of what `shared/` already offers so new/edited plugins reach for
|
||||
it instead of reinventing, and (2) list concrete cases where 2+ plugins built the same thing
|
||||
independently, so we know what's actually worth promoting to `shared/` next.
|
||||
|
||||
## 1. Reference: what's already in `shared/` and who uses it
|
||||
|
||||
| Module | What it does | Adoption |
|
||||
|---|---|---|
|
||||
| `ui/table.ts` | `TableRow`/`TableRowGroup`/`TableSwitchRow`/`TableRadioRow` — Discord's redesigned settings rows, falling back to legacy `Forms.Form*` | 24 files — well adopted, the default for any settings screen |
|
||||
| `ui/SettingsScaffold.tsx` | ScrollView + bottom padding wrapper for a plugin's settings screen | 19 files — well adopted |
|
||||
| `lib/color.ts` | `isValidHex`, `normalizeHex`, `resolveSemanticColor`/`resolveSemanticColorSafe` (theme-aware color token resolution) | 17 files — well adopted |
|
||||
| `ui/NoteBox.tsx` | Bordered callout box for explanatory settings text | 15 files — well adopted |
|
||||
| `ui/PrimaryButton.tsx` | Standard filled action button | 9 files — well adopted |
|
||||
| `lib/rawFind.ts` | Walks `window.modules` directly for a module lookup that survives Revenge's negative-result caching | 8 files |
|
||||
| `lib/patcher.ts` | `safePatch`/`applyPatches` — one broken patch surface can't crash a whole plugin's `onLoad` | 8 files — every plugin should use this for `onLoad`, worth spot-checking stragglers |
|
||||
| `lib/createElementIntercept.ts` | Intercepts `React.createElement`/`jsx`/`jsxs` to replace or transform components that aren't exported and can't be `after()`-patched directly | 7 files |
|
||||
| `lib/flux.ts` | `fluxSubscribe(event, cb, once?)` with built-in unsubscribe | 4 files |
|
||||
| `lib/waitFor.ts` | Polls until a lazily-registered module/value appears, cancelable | 4 files |
|
||||
| `ui/ListSection.tsx` | Titled section of tappable rows with an empty-state note | 3 files |
|
||||
| `lib/lazy.ts` | Memoizing wrapper for a Metro lookup (resolve once, cache forever) | 2 files — low adoption, worth checking for plugins hand-rolling the same "cache after first resolve" pattern |
|
||||
| `lib/patchRows.ts` | `patchRows(handler)` — hooks the native chat row bridge (`DCDChatManager.updateRows` / `RowManager.generate`) so a handler sees every message row as JSON | Used by rose-utils, role-color-everywhere, message-logger |
|
||||
| `ui/ColorInput.tsx` | Hex color field with swatches | **1 file (radial-status)** — see finding #2, this is the stale version |
|
||||
|
||||
**Note on `patchRows.ts` vs the member-list "UserRow" pattern below:** these are unrelated despite
|
||||
similar names. `patchRows` is for the *native chat message row bridge* (JSON crossing to native
|
||||
code). The `findByTypeNameAll("UserRow")` pattern in finding #1 is a *React component* used in the
|
||||
member list / profile popout. Don't conflate them when extracting shared code.
|
||||
|
||||
## 2. Findings: duplicated or fork-worthy code
|
||||
|
||||
### Finding 1 — `staff-tags` and `custom-user-tags` duplicate five files byte-for-byte
|
||||
`ui/IconPicker.tsx`, `ui/Icon.tsx`, `lib/icons.ts`, and `patches/tag.tsx` are **100% identical**
|
||||
between the two plugins (confirmed via `diff`, zero output). `ui/ColorInput.tsx` is also identical
|
||||
between the two but has drifted from `shared/`'s version — see finding 2.
|
||||
|
||||
`patches/name.tsx`, `patches/details.tsx`, `patches/chat.ts`, `patches/profile.tsx` are not
|
||||
byte-identical but are structurally the same patch applied twice: same target modules
|
||||
(`DisplayName`, `UserRow`, `getTagProperties`, `UserProfilePrimaryInfo`), same comments explaining
|
||||
*why* (e.g. "isn't a top-level export" copied verbatim into both `profile.tsx` files), and — not
|
||||
in scope for this audit, but worth knowing — the **same latent bug** in both: the
|
||||
`existingTag.props?.type !== 0` guard can't tell "a real Discord tag is here" from "the other
|
||||
plugin already set a tag here," so having both plugins enabled causes one to silently clobber the
|
||||
other's tag on a user. That's the parked staff-tags/custom-user-tags coexistence design (separate
|
||||
task).
|
||||
|
||||
**Recommendation:** promote `IconPicker.tsx`, `Icon.tsx`, `icons.ts` to `shared/ui` and `shared/lib`
|
||||
as-is (genuine, no-decision-needed duplication). Leave the four patch files alone for now — their
|
||||
near-duplication is entangled with the coexistence bug fix, so refactor them together with that
|
||||
work rather than as a mechanical move.
|
||||
|
||||
### Finding 2 — `shared/ui/ColorInput.tsx` is a stale stub; the real version lives unshared in two plugins
|
||||
`shared/ui/ColorInput.tsx` is a minimal 2-swatch color field. `staff-tags/src/ui/ColorInput.tsx`
|
||||
and `custom-user-tags/src/ui/ColorInput.tsx` (identical to each other) are a fuller implementation:
|
||||
28 swatches, hex↔RGB conversion helpers, uses `normalizeHex` from `shared/lib/color`. Only
|
||||
`radial-status` uses the shared stub today; everyone who needs a real color picker forked instead
|
||||
of upstreaming.
|
||||
|
||||
**Recommendation:** replace `shared/ui/ColorInput.tsx`'s contents with the fuller version, point
|
||||
`staff-tags`/`custom-user-tags`/`radial-status` at it, delete the two local copies. Check
|
||||
`radial-status`'s usage still renders correctly with the richer swatch set before removing its
|
||||
reliance on the stub.
|
||||
|
||||
### Finding 3 — `server-drawer` forked `createElementIntercept.ts` instead of extending the shared one
|
||||
`server-drawer/src/patches/createElementIntercept.ts` is a deliberate fork of
|
||||
`shared/lib/createElementIntercept.ts`, not accidental drift: it adds a `collapseAncestors` option
|
||||
(ancestor-collapsing, used to hide the guilds bar and reclaim its layout space) that the shared
|
||||
version doesn't have. 88 lines of diff, all additive on server-drawer's side.
|
||||
|
||||
**Recommendation:** this is a real capability gap in `shared/`, not a copy-paste duplication. Worth
|
||||
upstreaming `collapseAncestors` into `shared/lib/createElementIntercept.ts` and switching
|
||||
server-drawer back onto the shared module — but do it deliberately (it's shared infra used by 7
|
||||
files across the repo; changing its behavior needs a look at every caller, not just server-drawer's
|
||||
four files that reference it).
|
||||
|
||||
### Finding 4 — per-ID storage-map boilerplate repeated in at least 5 plugins
|
||||
The pattern `storage.<key> ??= {}` followed by hand-written get/set/(sometimes remove) helpers for
|
||||
a per-user or per-ID settings map shows up independently in:
|
||||
- `staff-tags/src/lib/getTag.ts` (`tagSettings(id)`)
|
||||
- `custom-user-tags/src/lib/tags.ts` (`allTags()`, `getUserTag()`, `setUserTag()`, `removeUserTag()`)
|
||||
- `message-snippets/src/lib/snippets.ts`
|
||||
- `radial-status/src/ui/Settings.tsx`
|
||||
- `message-logger/src/index.ts` (single-key, simpler case)
|
||||
|
||||
**Recommendation:** a small `shared/lib/storageMap.ts` exporting something like
|
||||
`createStorageMap<T>(storage, key)` → `{ get(id), set(id, value), remove(id), all() }` would
|
||||
collapse ~15-20 lines per plugin into one call. Low risk, no behavior change, good first
|
||||
extraction to pair with finding 1's icon files.
|
||||
|
||||
### Finding 5 — superseded, `raidens-themes` removed
|
||||
Originally: `raidens-themes/src/patches/settings.ts` and `rose-plugs/src/patches/settings.ts`
|
||||
independently solved "pin a settings entry into Discord's native App Settings" with different,
|
||||
non-trivial APIs (272 lines of diff). `raidens-themes` was deleted from the repo on 2026-08-16, so
|
||||
this is no longer a duplication to reconcile — `rose-plugs/src/patches/settings.ts` is now the only
|
||||
implementation. Worth revisiting only if a future plugin wants the same capability; its
|
||||
`SectionRow[]` API (`patches/settings.ts` exports `patchRosiesPlugsSection`) is the one to build on,
|
||||
not extract into `shared/` preemptively with no second consumer.
|
||||
|
||||
## 3. Suggested order if/when this gets acted on
|
||||
1. Finding 4 (storage map) — mechanical, zero behavior change, unblocks nothing else.
|
||||
2. Finding 1's static files (icons/IconPicker) — mechanical, identical code, low risk.
|
||||
3. Finding 2 (ColorInput) — mechanical but touches 3 plugins' rendering, needs a visual check.
|
||||
4. Finding 3 (createElementIntercept collapseAncestors) — real shared-infra change, needs care.
|
||||
5. Finding 1's patch files (name/details/chat/profile) — do together with the parked
|
||||
staff-tags/custom-user-tags coexistence design, not standalone.
|
||||
6. Finding 5 (settings pinning) — defer until a third consumer exists.
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchRowManager from "./patches/rowManager";
|
||||
import patchTimestamp from "./patches/timestamp";
|
||||
import Settings from "./ui/Settings";
|
||||
|
|
@ -13,10 +14,10 @@ export default {
|
|||
storage.customFormat ??= "dddd, MMMM Do YYYY, h:mm:ss a";
|
||||
storage.separateMessages ??= false;
|
||||
|
||||
unpatchAll = applyPatches("BetterTimestamps", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("BetterTimestamps", logger, {
|
||||
"message & day timestamps": patchRowManager,
|
||||
"message timestamp component": patchTimestamp
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { findByName, findByStoreName } from "@vendetta/metro";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
|
|
@ -6,6 +7,7 @@ import { clipboard } from "@vendetta/metro/common";
|
|||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
|
||||
|
||||
// A gradient role's real colors live at role.colorStrings ({primaryColor, secondaryColor,
|
||||
|
|
@ -119,10 +121,10 @@ let unpatchAll: () => void = () => {};
|
|||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unpatchAll = applyPatches("CopyRoleColor", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("CopyRoleColor", logger, {
|
||||
"role pill long-press": patchRolePill,
|
||||
"profile role item long-press": patchProfileRoleItem
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchChat from "./patches/chat";
|
||||
import patchName from "./patches/name";
|
||||
import patchDetails from "./patches/details";
|
||||
|
|
@ -14,13 +15,13 @@ export default {
|
|||
onLoad: () => {
|
||||
storage.tags ??= {};
|
||||
|
||||
unpatchAll = applyPatches("Custom User Tags", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("Custom User Tags", logger, {
|
||||
tag: patchTag,
|
||||
chat: patchChat,
|
||||
name: patchName,
|
||||
details: patchDetails,
|
||||
profile: patchProfile
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import { FluxDispatcher } from "@fpte/lib/flux";
|
||||
import { UserProfileStore, UserStore } from "@fpte/lib/stores";
|
||||
import {
|
||||
|
|
@ -36,7 +37,7 @@ export default {
|
|||
// self-contained fallback picker instead.
|
||||
storage.forceFallbackEffectPicker ??= true;
|
||||
|
||||
unpatchAll = applyPatches("FakeProfileThemesAndEffects", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("FakeProfileThemesAndEffects", logger, {
|
||||
"purchase check": patchGetPurchase,
|
||||
"user profile data": patchGetUserProfile,
|
||||
"profile effect sections": () => {
|
||||
|
|
@ -46,7 +47,7 @@ export default {
|
|||
"profile theme": patchUseProfileTheme,
|
||||
"profile edit form": patchUserProfileEditForm,
|
||||
"nitro upsell card": patchNitroUpsellCard
|
||||
});
|
||||
}));
|
||||
updateProfileThemeAndEffect();
|
||||
},
|
||||
onUnload() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { id } from "@vendetta/plugin";
|
||||
import { checkForUpdate } from "@shared/lib/reload";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
checkForUpdate(id).catch(() => {});
|
||||
},
|
||||
settings: Settings
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import Settings from "./ui/Settings";
|
||||
import { patchFluxIntercept, revertFakedMessages } from "./patches/fluxIntercept";
|
||||
import { patchFluxIntercept, rehydrateFromLog, revertFakedMessages } from "./patches/fluxIntercept";
|
||||
import { patchRowStyling } from "./patches/rowStyling";
|
||||
import { initCloudSync } from "./lib/cloudSync";
|
||||
|
||||
const TAG = "[MessageLogger]";
|
||||
const cleanups: (() => void)[] = [];
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
function initStorage() {
|
||||
storage.options ??= {};
|
||||
const o = storage.options;
|
||||
o.logDeleted ??= true;
|
||||
o.keepDeletedInline ??= true;
|
||||
o.keepDeletedInline ??= false;
|
||||
o.logEdited ??= true;
|
||||
o.ignoreBots ??= false;
|
||||
o.ignoreOwnMessages ??= false;
|
||||
|
|
@ -31,11 +33,20 @@ export default {
|
|||
initStorage();
|
||||
console.log(TAG, "onLoad");
|
||||
|
||||
let patched = 0;
|
||||
if (patchFluxIntercept(cleanups)) patched++;
|
||||
if (patchRowStyling(cleanups)) patched++;
|
||||
unpatchAll = guardPlugin(id, () => {
|
||||
const cleanups: (() => void)[] = [];
|
||||
let patched = 0;
|
||||
if (patchFluxIntercept(cleanups)) patched++;
|
||||
if (patchRowStyling(cleanups)) patched++;
|
||||
cleanups.push(initCloudSync());
|
||||
rehydrateFromLog();
|
||||
|
||||
console.log(TAG, `onLoad done - ${patched}/2 patches applied`);
|
||||
console.log(TAG, `onLoad done - ${patched}/2 patches applied`);
|
||||
return () => {
|
||||
for (const fn of cleanups) fn();
|
||||
cleanups.length = 0;
|
||||
};
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
console.log(TAG, "onUnload");
|
||||
|
|
@ -44,8 +55,7 @@ export default {
|
|||
} catch (e: any) {
|
||||
logger.error("[MessageLogger] Failed to revert faked messages:", e?.message ?? e);
|
||||
}
|
||||
for (const fn of cleanups) fn();
|
||||
cleanups.length = 0;
|
||||
unpatchAll();
|
||||
},
|
||||
settings: Settings,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { id } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import loadCommands from "./commands";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let unregisterFns: (() => void)[] = [];
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unregisterFns = loadCommands();
|
||||
unpatchAll = guardPlugin(id, () => {
|
||||
const unregisterFns = loadCommands();
|
||||
return () => unregisterFns.forEach((fn) => fn());
|
||||
});
|
||||
},
|
||||
onUnload: () => unregisterFns.forEach((fn) => fn()),
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import Settings from "./ui/Settings";
|
||||
import patchMoreAltsSection, { SectionRow } from "./patches/sidebarPin";
|
||||
import patchContextMenu from "./patches/contextMenu";
|
||||
|
|
@ -7,10 +9,7 @@ import patchNativeSwitcher from "./lib/nativeSwitcher";
|
|||
import loadCommands from "./commands";
|
||||
import { ensureStorage, getSettings } from "./lib/accounts";
|
||||
|
||||
let unpatchSidebar: (() => void) | undefined;
|
||||
let unpatchContextMenu: (() => void) | undefined;
|
||||
let unpatchNativeSwitcher: (() => void) | undefined;
|
||||
let unregisterCommands: (() => void)[] = [];
|
||||
let teardown: () => void = () => {};
|
||||
|
||||
function buildRows(): SectionRow[] {
|
||||
if (!getSettings().addToSidebar) return [];
|
||||
|
|
@ -30,36 +29,47 @@ export default {
|
|||
onLoad() {
|
||||
ensureStorage();
|
||||
|
||||
try {
|
||||
unpatchSidebar = patchMoreAltsSection(buildRows);
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch sidebar:", e?.message ?? e);
|
||||
}
|
||||
teardown = guardPlugin(id, () => {
|
||||
let unpatchSidebar: (() => void) | undefined;
|
||||
let unpatchContextMenu: (() => void) | undefined;
|
||||
let unpatchNativeSwitcher: (() => void) | undefined;
|
||||
let unregisterCommands: (() => void)[] = [];
|
||||
|
||||
try {
|
||||
unpatchContextMenu = patchContextMenu();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch context menu:", e?.message ?? e);
|
||||
}
|
||||
try {
|
||||
unpatchSidebar = patchMoreAltsSection(buildRows);
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch sidebar:", e?.message ?? e);
|
||||
}
|
||||
|
||||
try {
|
||||
unpatchNativeSwitcher = patchNativeSwitcher();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch native switcher:", e?.message ?? e);
|
||||
}
|
||||
try {
|
||||
unpatchContextMenu = patchContextMenu();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch context menu:", e?.message ?? e);
|
||||
}
|
||||
|
||||
try {
|
||||
unregisterCommands = loadCommands();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to register commands:", e?.message ?? e);
|
||||
}
|
||||
try {
|
||||
unpatchNativeSwitcher = patchNativeSwitcher();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to patch native switcher:", e?.message ?? e);
|
||||
}
|
||||
|
||||
try {
|
||||
unregisterCommands = loadCommands();
|
||||
} catch (e: any) {
|
||||
logger.error("[MoreAlts] Failed to register commands:", e?.message ?? e);
|
||||
}
|
||||
|
||||
return () => {
|
||||
unpatchSidebar?.();
|
||||
unpatchContextMenu?.();
|
||||
unpatchNativeSwitcher?.();
|
||||
unregisterCommands.forEach((u) => u());
|
||||
unregisterCommands = [];
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
unpatchSidebar?.();
|
||||
unpatchContextMenu?.();
|
||||
unpatchNativeSwitcher?.();
|
||||
unregisterCommands.forEach((u) => u());
|
||||
unregisterCommands = [];
|
||||
teardown();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
import { rawFindByTypeName } from "@shared/lib/rawFind";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import PronounSection from "./ui/PronounSection";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
|
|
@ -66,20 +68,25 @@ function attempt() {
|
|||
}
|
||||
}
|
||||
|
||||
let teardown: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
attempt();
|
||||
if (!patched) {
|
||||
let ticks = 0;
|
||||
retryHandle = setInterval(() => {
|
||||
attempt();
|
||||
if (++ticks >= 30) stopRetrying(); // ~9s at 300ms, then give up
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
onUnload: () => {
|
||||
stopRetrying();
|
||||
unpatch();
|
||||
teardown = guardPlugin(id, () => {
|
||||
attempt();
|
||||
if (!patched) {
|
||||
let ticks = 0;
|
||||
retryHandle = setInterval(() => {
|
||||
attempt();
|
||||
if (++ticks >= 30) stopRetrying(); // ~9s at 300ms, then give up
|
||||
}, 300);
|
||||
}
|
||||
return () => {
|
||||
stopRetrying();
|
||||
unpatch();
|
||||
};
|
||||
});
|
||||
},
|
||||
onUnload: () => teardown(),
|
||||
settings: Settings
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { findByStoreName } from "@vendetta/metro";
|
||||
import { checkPluginStatus } from "@shared/lib/backend";
|
||||
import { checkForUpdate } from "@shared/lib/reload";
|
||||
|
||||
import Settings from "./ui/Settings";
|
||||
import { completeAllVideoQuests } from "./lib/questCompleter";
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
checkForUpdate(id).catch(() => {});
|
||||
|
||||
// Fire-and-forget: never blocks Discord's own boot, and a failure here (e.g. QuestStore
|
||||
// not populated yet this session) is logged, not thrown - onLoad must not crash the app.
|
||||
completeAllVideoQuests().catch((e) => {
|
||||
logger.error(`[QuestOrbs] Auto-run on load failed: ${e}`);
|
||||
checkPluginStatus(UserStore?.getCurrentUser?.()?.id, id).then((status) => {
|
||||
if (status.blocked) return;
|
||||
completeAllVideoQuests().catch((e) => {
|
||||
logger.error(`[QuestOrbs] Auto-run on load failed: ${e}`);
|
||||
});
|
||||
});
|
||||
},
|
||||
onUnload: () => {},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchRing from "./patches/ringPatch";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
|
|
@ -21,9 +22,9 @@ export default {
|
|||
};
|
||||
storage.ringThickness ??= 2;
|
||||
|
||||
unpatchAll = applyPatches("Radial Status", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("Radial Status", logger, {
|
||||
ring: patchRing
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
import { id } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import loadCommands from "./commands";
|
||||
import { startScheduler, stopScheduler } from "./lib/reminders";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let unregisterFns: (() => void)[] = [];
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unregisterFns = loadCommands();
|
||||
startScheduler();
|
||||
},
|
||||
onUnload: () => {
|
||||
unregisterFns.forEach((fn) => fn());
|
||||
stopScheduler();
|
||||
unpatchAll = guardPlugin(id, () => {
|
||||
const unregisterFns = loadCommands();
|
||||
startScheduler();
|
||||
return () => {
|
||||
unregisterFns.forEach((fn) => fn());
|
||||
stopScheduler();
|
||||
};
|
||||
});
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { id } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import loadCommands from "./commands";
|
||||
|
||||
let unregisterFns: (() => void)[] = [];
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unregisterFns = loadCommands();
|
||||
unpatchAll = guardPlugin(id, () => {
|
||||
const unregisterFns = loadCommands();
|
||||
return () => unregisterFns.forEach((fn) => fn());
|
||||
});
|
||||
},
|
||||
onUnload: () => unregisterFns.forEach((fn) => fn())
|
||||
onUnload: () => unpatchAll()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchRows from "./patches/rows";
|
||||
import patchMemberList from "./patches/memberList";
|
||||
import patchTypingWrapper from "./patches/typingWrapper";
|
||||
|
|
@ -17,12 +18,12 @@ export default {
|
|||
storage.hideTyping ??= false;
|
||||
storage.chatInterpolation ??= 0;
|
||||
|
||||
unpatchAll = applyPatches("RoleColorEverywhere", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("RoleColorEverywhere", logger, {
|
||||
"mentions & chat text": patchRows,
|
||||
"member list role headers": patchMemberList,
|
||||
"typing indicator": patchTypingWrapper,
|
||||
"voice channel names": patchVoiceUserConnected
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import Settings from "./ui/Settings";
|
||||
import PluginsBrowser from "./ui/PluginsBrowser";
|
||||
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
|
||||
import { discoverRosiesPlugins } from "./patches/discoverPlugins";
|
||||
|
||||
let unpatch: (() => void) | undefined;
|
||||
let teardown: () => void = () => {};
|
||||
|
||||
function buildRows(): SectionRow[] {
|
||||
const rows: SectionRow[] = [
|
||||
|
|
@ -38,15 +40,18 @@ function buildRows(): SectionRow[] {
|
|||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
try {
|
||||
unpatch = patchRosiesPlugsSection(buildRows);
|
||||
} catch (e: any) {
|
||||
logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e);
|
||||
}
|
||||
teardown = guardPlugin(id, () => {
|
||||
let unpatch: (() => void) | undefined;
|
||||
try {
|
||||
unpatch = patchRosiesPlugsSection(buildRows);
|
||||
} catch (e: any) {
|
||||
logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e);
|
||||
}
|
||||
return () => unpatch?.();
|
||||
});
|
||||
},
|
||||
onUnload: () => {
|
||||
unpatch?.();
|
||||
unpatch = undefined;
|
||||
teardown();
|
||||
},
|
||||
settings: Settings,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,28 +1,37 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import modules from "./modules";
|
||||
import { vstorage } from "./lib/Module";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let teardown: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
vstorage.modules ??= {};
|
||||
|
||||
for (const module of modules) {
|
||||
try {
|
||||
if (module.storage.enabled) module.start();
|
||||
} catch (e) {
|
||||
logger.error(`[RoseUtils] Failed to start "${module.label}" on load: ${e}`);
|
||||
teardown = guardPlugin(id, () => {
|
||||
for (const module of modules) {
|
||||
try {
|
||||
if (module.storage.enabled) module.start();
|
||||
} catch (e) {
|
||||
logger.error(`[RoseUtils] Failed to start "${module.label}" on load: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
for (const module of modules) {
|
||||
try {
|
||||
module.stop();
|
||||
} catch (e) {
|
||||
logger.error(`[RoseUtils] Failed to stop "${module.label}" cleanly: ${e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
onUnload() {
|
||||
for (const module of modules) {
|
||||
try {
|
||||
module.stop();
|
||||
} catch (e) {
|
||||
logger.error(`[RoseUtils] Failed to stop "${module.label}" cleanly: ${e}`);
|
||||
}
|
||||
}
|
||||
teardown();
|
||||
},
|
||||
settings: Settings,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchChat from "./patches/chat";
|
||||
import patchDetails from "./patches/details";
|
||||
import patchName from "./patches/name";
|
||||
|
|
@ -15,13 +16,13 @@ export default {
|
|||
storage.useRoleColor ??= false;
|
||||
storage.tags ??= {};
|
||||
|
||||
unpatchAll = applyPatches("Staff Tags", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("Staff Tags", logger, {
|
||||
tag: patchTag,
|
||||
chat: patchChat,
|
||||
name: patchName,
|
||||
details: patchDetails,
|
||||
profile: patchProfile
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchTypingIndicator from "./patches/typingIndicator";
|
||||
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unpatchAll = applyPatches("TypingAvatars", logger, {
|
||||
unpatchAll = guardPlugin(id, () => applyPatches("TypingAvatars", logger, {
|
||||
"typing indicator avatars": patchTypingIndicator
|
||||
});
|
||||
}));
|
||||
},
|
||||
onUnload: () => unpatchAll()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { id } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import loadCommands from "./commands";
|
||||
|
||||
let unregisterFns: (() => void)[] = [];
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
unregisterFns = loadCommands();
|
||||
unpatchAll = guardPlugin(id, () => {
|
||||
const unregisterFns = loadCommands();
|
||||
return () => unregisterFns.forEach((fn) => fn());
|
||||
});
|
||||
},
|
||||
onUnload: () => unregisterFns.forEach((fn) => fn())
|
||||
onUnload: () => unpatchAll()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// Forked from bwlok/revenge-plugins (plugins/ViewRaw), GPLv3. Original authors: sapphire, Vendicated, Bwlok.
|
||||
// https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw
|
||||
import { before, after } from "@vendetta/patcher";
|
||||
import { id } from "@vendetta/plugin";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
import { findByName, findByProps } from "@vendetta/metro";
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import RawPage from "./RawPage";
|
||||
import { recordDetection } from "./lib/diagnostics";
|
||||
import Settings from "./ui/Settings";
|
||||
|
|
@ -64,9 +66,10 @@ function actionSheetRowButton(navigator: () => JSX.Element) {
|
|||
);
|
||||
}
|
||||
|
||||
const patches: (() => void)[] = [];
|
||||
function applyViewRawPatches(): () => void {
|
||||
const patches: (() => void)[] = [];
|
||||
|
||||
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
|
||||
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
|
||||
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
|
||||
|
||||
component.then((instance: any) => {
|
||||
|
|
@ -156,7 +159,15 @@ const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, ms
|
|||
|
||||
patches.push(unpatchOpenLazy);
|
||||
|
||||
return () => patches.forEach((unpatch) => unpatch());
|
||||
}
|
||||
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onUnload: () => patches.forEach((unpatch) => unpatch()),
|
||||
onLoad: () => {
|
||||
unpatchAll = guardPlugin(id, applyViewRawPatches);
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,38 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { id, storage } from "@vendetta/plugin";
|
||||
import { guardPlugin } from "@shared/lib/guard";
|
||||
import patchYouBarButtons from "./patches/youBarButtons";
|
||||
import { setInboxTracking } from "./lib/notifications";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let unpatchButtons: () => void = () => {};
|
||||
let patched = false;
|
||||
let retryHandle: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
function stopRetrying() {
|
||||
if (retryHandle) {
|
||||
clearInterval(retryHandle);
|
||||
retryHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// YouBarNotificationsButton isn't registered until YouBar itself has rendered at least once -
|
||||
// retries on an interval until it lands.
|
||||
function attempt() {
|
||||
try {
|
||||
const unpatch = patchYouBarButtons();
|
||||
if (unpatch) {
|
||||
unpatchButtons = unpatch;
|
||||
patched = true;
|
||||
stopRetrying();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`[YouBar+] Failed to apply the "YouBar buttons" patch: ${e}`);
|
||||
stopRetrying();
|
||||
}
|
||||
}
|
||||
|
||||
let teardown: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
|
|
@ -15,14 +43,23 @@ export default {
|
|||
|
||||
setInboxTracking(!!storage.showInboxButton);
|
||||
|
||||
try {
|
||||
unpatchButtons = patchYouBarButtons();
|
||||
} catch (e) {
|
||||
logger.error(`[YouBar+] Failed to apply the "YouBar buttons" patch: ${e}`);
|
||||
}
|
||||
teardown = guardPlugin(id, () => {
|
||||
attempt();
|
||||
if (!patched) {
|
||||
let ticks = 0;
|
||||
retryHandle = setInterval(() => {
|
||||
attempt();
|
||||
if (++ticks >= 30) stopRetrying(); // ~9s at 300ms, then give up
|
||||
}, 300);
|
||||
}
|
||||
return () => {
|
||||
stopRetrying();
|
||||
unpatchButtons();
|
||||
};
|
||||
});
|
||||
},
|
||||
onUnload: () => {
|
||||
unpatchButtons();
|
||||
teardown();
|
||||
setInboxTracking(false);
|
||||
},
|
||||
settings: Settings
|
||||
|
|
|
|||
48
shared/lib/backend.ts
Normal file
48
shared/lib/backend.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const BASE_URL = "https://revenge-plugins-backend.allyapp.workers.dev";
|
||||
|
||||
export interface PluginStatus {
|
||||
blocked: boolean;
|
||||
latestHash: string | null;
|
||||
}
|
||||
|
||||
export async function checkPluginStatus(userId: string | undefined, manifestUrl?: string): Promise<PluginStatus> {
|
||||
if (!userId) return { blocked: true, latestHash: null };
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ userId });
|
||||
if (manifestUrl) params.set("manifestUrl", manifestUrl);
|
||||
|
||||
const res = await fetch(`${BASE_URL}/status?${params}`);
|
||||
if (!res.ok) return { blocked: true, latestHash: null };
|
||||
|
||||
const data = await res.json();
|
||||
return { blocked: data?.blocked !== false, latestHash: data?.latestHash ?? null };
|
||||
} catch {
|
||||
return { blocked: true, latestHash: null };
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncPull(pluginId: string, authToken: string): Promise<unknown> {
|
||||
const res = await fetch(`${BASE_URL}/sync/${pluginId}`, {
|
||||
headers: { Authorization: authToken }
|
||||
});
|
||||
if (!res.ok) throw new Error(`Sync pull failed: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function syncPush(pluginId: string, authToken: string, data: unknown): Promise<void> {
|
||||
const res = await fetch(`${BASE_URL}/sync/${pluginId}`, {
|
||||
method: "PUT",
|
||||
headers: { Authorization: authToken, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) throw new Error(`Sync push failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export async function syncDelete(pluginId: string, authToken: string): Promise<void> {
|
||||
const res = await fetch(`${BASE_URL}/sync/${pluginId}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: authToken }
|
||||
});
|
||||
if (!res.ok) throw new Error(`Sync delete failed: ${res.status}`);
|
||||
}
|
||||
23
shared/lib/guard.ts
Normal file
23
shared/lib/guard.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { findByStoreName } from "@vendetta/metro";
|
||||
import { checkPluginStatus } from "./backend";
|
||||
import { checkForUpdate } from "./reload";
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
export function guardPlugin(pluginId: string, applyPatches: () => () => void): () => void {
|
||||
let unpatch = applyPatches();
|
||||
let unloaded = false;
|
||||
|
||||
checkPluginStatus(UserStore?.getCurrentUser?.()?.id, pluginId).then((status) => {
|
||||
if (unloaded || !status.blocked) return;
|
||||
unpatch();
|
||||
unpatch = () => {};
|
||||
});
|
||||
|
||||
checkForUpdate(pluginId).catch(() => {});
|
||||
|
||||
return () => {
|
||||
unloaded = true;
|
||||
unpatch();
|
||||
};
|
||||
}
|
||||
18
shared/lib/reload.ts
Normal file
18
shared/lib/reload.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { fetchPlugin, plugins, startPlugin, stopPlugin } from "@vendetta/plugins";
|
||||
import { findByStoreName } from "@vendetta/metro";
|
||||
import { checkPluginStatus } from "./backend";
|
||||
|
||||
const UserStore = findByStoreName("UserStore");
|
||||
|
||||
export async function checkForUpdate(pluginId: string): Promise<void> {
|
||||
const plugin = plugins[pluginId];
|
||||
if (!plugin) return;
|
||||
|
||||
const userId = UserStore?.getCurrentUser?.()?.id;
|
||||
const status = await checkPluginStatus(userId, pluginId);
|
||||
if (status.blocked || !status.latestHash || status.latestHash === plugin.manifest.hash) return;
|
||||
|
||||
stopPlugin(pluginId);
|
||||
await fetchPlugin(pluginId);
|
||||
await startPlugin(pluginId);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue