Delete plugins/quest-orbs directory
This commit is contained in:
parent
4981833a38
commit
82eec74856
5 changed files with 0 additions and 366 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue