Delete plugins/quest-orbs directory

This commit is contained in:
Rosie Val 2026-08-27 08:05:37 -04:00 • committed by GitHub
parent 4981833a38
commit 82eec74856
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 0 additions and 366 deletions

View file

@ -1,11 +0,0 @@
{
"name": "Quest Orbs",
"description": "WARNING: THIS CAN GET YOUR ACCOUNT BANNED. Automates completion of mobile-video Discord Quests and claims the Orb rewards - real-time paced (no accelerated/faked timestamps), so the traffic mirrors a real client, but this is still client-modification automation that violates Discord's Terms of Service and can get the account it runs on suspended or banned. If claiming a reward triggers Discord's captcha challenge, this plugin never tries to solve or bypass it - it stops immediately and you complete that captcha yourself, directly in Discord's normal Quests screen. Use only on an account you're willing to risk.",
"authors": [
{ "name": "Rosie" }
],
"main": "src/index.ts",
"vendetta": {
"icon": "ic_reward_redeemed"
}
}

View file

@ -1,16 +0,0 @@
import { logger } from "@vendetta";
import Settings from "./ui/Settings";
import { completeAllVideoQuests } from "./lib/questCompleter";
export default {
onLoad: () => {
// 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}`);
});
},
onUnload: () => {},
settings: Settings
};

View file

@ -1,83 +0,0 @@
import { rawFindByStoreName } from "@shared/lib/rawFind";
import { lazy } from "@shared/lib/lazy";
declare const window: any;
export interface RestResponse<T = any> {
ok: boolean;
status: number;
body: T;
headers?: any;
text?: string;
retryAfter?: number;
}
export interface RestClient {
get(opts: { url: string; query?: any }): Promise<RestResponse>;
post(opts: { url: string; body?: any }): Promise<RestResponse>;
}
export interface QuestActions {
fetchCurrentQuests?: (...args: any[]) => any;
}
// rawFindByProps only checks that named properties exist, not that they're real functions - a
// decoy module can match with e.g. "get" set to null. Also requiring a real getAPIBaseURL function
// plus a V8APIError/V6OrEarlierAPIError export rules those decoys out.
export const getRestClient = lazy<RestClient>(() => {
const modules = window?.modules;
if (!modules) return undefined;
for (const id in modules) {
const def = modules[id];
if (!def?.isInitialized) continue;
const exports = def.publicModule?.exports;
if (!exports) continue;
for (const candidate of [exports, exports.default]) {
if (!candidate) continue;
if (
typeof candidate.get === "function" &&
typeof candidate.post === "function" &&
typeof candidate.getAPIBaseURL === "function" &&
(candidate.V8APIError || candidate.V6OrEarlierAPIError)
) {
return candidate as RestClient;
}
}
}
return undefined;
});
export const getQuestStore = lazy<any>(() => rawFindByStoreName("QuestStore"));
// Only used for an optional, best-effort refresh - completion itself goes through direct REST
// calls, not this module. Calling a function on this exports object directly (as an external
// caller) works fine; PATCHING it wouldn't, since same-chunk internal calls bypass the exports
// object.
export const getQuestActions = lazy<QuestActions>(() => {
const modules = window?.modules;
if (!modules) return undefined;
for (const id in modules) {
const def = modules[id];
if (!def?.isInitialized) continue;
const exports = def.publicModule?.exports;
if (!exports) continue;
for (const candidate of [exports, exports.default]) {
if (!candidate) continue;
if (
typeof candidate.enrollInQuest === "function" &&
typeof candidate.updateVideoProgress === "function" &&
typeof candidate.claimQuestReward === "function" &&
typeof candidate.fetchCurrentQuests === "function"
) {
return candidate as QuestActions;
}
}
}
return undefined;
});

View file

@ -1,154 +0,0 @@
import { logger } from "@vendetta";
import { showToast } from "@vendetta/ui/toasts";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { getRestClient, getQuestStore, getQuestActions } from "./modules";
const TAG = "[QuestOrbs]";
// Matches Discord's own ~7-8s video-progress heartbeat cadence - real-time paced, never sped up.
const HEARTBEAT_INTERVAL_MS = 8000;
export interface QuestRunResult {
completed: string[];
needsManualClaim: string[];
failed: string[];
skipped: number;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isVideoQuest(quest: any): boolean {
return !!quest?.config?.taskConfigV2?.tasks?.WATCH_VIDEO_ON_MOBILE;
}
function isAlreadyClaimed(quest: any): boolean {
return !!quest?.userStatus?.claimedAt;
}
function questName(quest: any): string {
return quest?.config?.messages?.questName ?? quest?.id ?? "unknown quest";
}
// Discord's wire format is snake_case, distinct from QuestStore's camelCase quest objects.
function questActionBody(quest: any, extra?: Record<string, unknown>) {
return {
location: 12,
is_targeted: false,
metadata_sealed: null,
traffic_metadata_sealed: quest.trafficMetadataSealed ?? null,
...extra,
};
}
async function enroll(rest: ReturnType<typeof getRestClient>, quest: any): Promise<void> {
await rest!.post({
url: `/quests/${quest.id}/enroll`,
body: questActionBody(quest),
});
}
// Sends real-time-paced heartbeats until completed_at flips non-null.
async function watchVideo(rest: ReturnType<typeof getRestClient>, quest: any): Promise<boolean> {
const target: number = quest.config.taskConfigV2.tasks.WATCH_VIDEO_ON_MOBILE.target;
let elapsed = 0;
while (elapsed < target) {
await sleep(HEARTBEAT_INTERVAL_MS);
elapsed = Math.min(elapsed + HEARTBEAT_INTERVAL_MS / 1000, target);
const res = await rest!.post({
url: `/quests/${quest.id}/video-progress`,
body: { timestamp: elapsed },
});
if (res?.body?.completed_at) return true;
}
return false;
}
type ClaimOutcome = "claimed" | "captcha" | "error";
// Detects a captcha_key response and stops - never attempts to solve or bypass it. Checks both a
// thrown error and a resolved-but-not-ok response, since which path the REST client takes on a
// non-2xx isn't pinned down.
async function claim(rest: ReturnType<typeof getRestClient>, quest: any): Promise<ClaimOutcome> {
try {
const res = await rest!.post({
url: `/quests/${quest.id}/claim-reward`,
body: questActionBody(quest, { platform: 0 }),
});
if (res?.ok) return "claimed";
if (res?.body?.captcha_key) return "captcha";
return "error";
} catch (e: any) {
if (e?.body?.captcha_key || e?.captcha_key) return "captcha";
logger.error(`${TAG} claim-reward request failed for "${questName(quest)}": ${e}`);
return "error";
}
}
// Best-effort refresh so Discord's UI doesn't show stale data after completing quests via direct
// REST calls instead of Discord's own action creators. Purely cosmetic.
function refreshQuestStore(): void {
try {
getQuestActions()?.fetchCurrentQuests?.();
} catch (e) {
logger.error(`${TAG} best-effort QuestStore refresh failed (non-fatal): ${e}`);
}
}
export async function completeAllVideoQuests(): Promise<QuestRunResult> {
const result: QuestRunResult = { completed: [], needsManualClaim: [], failed: [], skipped: 0 };
const rest = getRestClient();
const questStore = getQuestStore();
if (!rest || !questStore?.quests) {
logger.error(`${TAG} REST client or QuestStore not available yet - Discord hasn't loaded the Quests feature in this session`);
return result;
}
const quests: any[] = Array.from(questStore.quests.values()).filter(isVideoQuest);
for (const quest of quests) {
try {
if (isAlreadyClaimed(quest)) {
result.skipped++;
continue;
}
if (!quest.userStatus?.enrolledAt) {
await enroll(rest, quest);
}
if (!quest.userStatus?.completedAt) {
const done = await watchVideo(rest, quest);
if (!done) {
result.failed.push(questName(quest));
continue;
}
}
const outcome = await claim(rest, quest);
if (outcome === "claimed") {
result.completed.push(questName(quest));
} else if (outcome === "captcha") {
result.needsManualClaim.push(questName(quest));
showToast(`"${questName(quest)}" needs manual claim (captcha) - open it in Discord`, getAssetIDByName("ic_warning_24px"));
} else {
result.failed.push(questName(quest));
}
} catch (e) {
logger.error(`${TAG} Failed on quest "${questName(quest)}": ${e}`);
result.failed.push(questName(quest));
}
}
if (result.completed.length > 0) refreshQuestStore();
return result;
}

View file

@ -1,102 +0,0 @@
import { React, ReactNative } from "@vendetta/metro/common";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
import PrimaryButton from "@shared/ui/PrimaryButton";
import NoteBox from "@shared/ui/NoteBox";
import { TableRowGroup } from "@shared/ui/table";
import { resolveSemanticColorSafe } from "@shared/lib/color";
import { completeAllVideoQuests, QuestRunResult } from "../lib/questCompleter";
const { View, Text } = ReactNative;
const textColor = () => resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
const warningColor = () => resolveSemanticColorSafe(["TEXT_DANGER", "TEXT_NORMAL"], "#f0b232");
// Deliberately the first thing on this screen, not folded into the NoteBox below it - this is a
// real ban risk, not fine print.
function BanWarning() {
return (
<View
style={{
marginHorizontal: 16,
marginTop: 12,
marginBottom: 4,
padding: 12,
borderRadius: 8,
borderWidth: 2,
borderColor: warningColor(),
backgroundColor: "rgba(240,178,50,0.08)"
}}
>
<Text style={{ fontSize: 13, fontWeight: "700", color: warningColor(), marginBottom: 4 }}>
WARNING: THIS CAN GET YOUR ACCOUNT BANNED
</Text>
<Text style={{ fontSize: 12.5, lineHeight: 18, color: textColor() }}>
Automating Quest completion is a form of client modification, which Discord's Terms
of Service prohibit. Discord can and does detect and act on this kind of automation -
using this plugin risks suspension or a permanent ban on the account it runs on.
Nothing here disguises the traffic as anything other than what it is; it just skips
the tedium of manually sitting through each video.{"\n\n"}
This plugin never solves or bypasses a captcha. When claiming a reward triggers
Discord's own captcha challenge, this plugin stops immediately and leaves that quest
for you - you open it in Discord's normal Quests screen and complete the captcha
yourself, exactly as if this plugin weren't involved at all.{"\n\n"}
Use only on an account you're willing to risk, and understand this is unsupported,
unofficial automation with no guarantee Discord won't act on it.
</Text>
</View>
);
}
function summarize(result: QuestRunResult): string {
const parts: string[] = [];
if (result.completed.length) parts.push(`${result.completed.length} completed: ${result.completed.join(", ")}`);
if (result.needsManualClaim.length) parts.push(`${result.needsManualClaim.length} need manual claim (captcha): ${result.needsManualClaim.join(", ")}`);
if (result.failed.length) parts.push(`${result.failed.length} failed: ${result.failed.join(", ")}`);
parts.push(`${result.skipped} already claimed`);
return parts.join("\n");
}
export default function Settings() {
const [running, setRunning] = React.useState(false);
const [lastResult, setLastResult] = React.useState<string | null>(null);
const run = async () => {
if (running) return;
setRunning(true);
setLastResult(null);
try {
const result = await completeAllVideoQuests();
setLastResult(summarize(result));
} catch (e) {
setLastResult(`Run failed: ${e}`);
} finally {
setRunning(false);
}
};
return (
<SettingsScaffold>
<BanWarning />
<TableRowGroup title="Quest Orbs">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton
label={running ? "Running..." : "Complete quests now"}
disabled={running}
onPress={run}
/>
{lastResult != null && (
<Text style={{ marginTop: 8, fontSize: 12.5, opacity: 0.85, color: textColor() }} selectable>
{lastResult}
</Text>
)}
</View>
</TableRowGroup>
<NoteBox>
Also runs automatically once when Discord loads. Only handles mobile video-watch
quests, paced in real time (no sped-up/faked progress) - see the warning above for
what "automatically" does and doesn't cover.
</NoteBox>
</SettingsScaffold>
);
}