├── components ├── QuestButton.css └── QuestButton.tsx ├── README.md ├── types ├── stores.d.ts ├── guildchannelstore.d.ts └── queststore.d.ts ├── stores.ts ├── settings.ts └── index.tsx /components/QuestButton.css: -------------------------------------------------------------------------------- 1 | .quest-button-enrollable>span[class*="iconBadge"] { 2 | background-color: var(--status-danger); 3 | } 4 | 5 | .quest-button-enrolled>span[class*="iconBadge"] { 6 | background-color: var(--status-warning); 7 | } 8 | 9 | .quest-button-claimable>span[class*="iconBadge"] { 10 | background-color: var(--status-positive); 11 | } 12 | 13 | .quest-button svg:has(> [mask^="url(#svg-mask-panel-button)"]) { 14 | display: none; 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CompleteDiscordQuest for Vencord 2 | 3 | This is a porting of the original BetterDiscord(BD) plugin [CompleteDiscordQuest](https://github.com/nicola02nb/BetterDiscord-Stuff/tree/main/Plugins/CompleteDiscordQuest). 4 | 5 | A Vencord(VC) plugin that completes you multiple discord quests in background simultaneously. 6 | 7 | ## Credits: 8 | 9 | This is a porting for BetterDiscord of a [snippet](https://gist.github.com/aamiaa/204cd9d42013ded9faf646fae7f89fbb) made by [aamiaa](https://github.com/aamiaa). 10 | 11 | ## Features: 12 | 13 | - Set inteval time to check for new quests 14 | -------------------------------------------------------------------------------- /types/stores.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export type ApplicationStreamingStore = any; 8 | export type RunningGameStore = any; 9 | export type QuestsStore = { 10 | addChangeListener: (listener: () => void) => void; 11 | removeChangeListener: (listener: () => void) => void; 12 | quests: Map; 13 | }; 14 | export type ChannelStore = any; 15 | export type GuildChannelStore = { 16 | getAllGuilds(): Map; 17 | }; 18 | -------------------------------------------------------------------------------- /stores.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { findByPropsLazy, findStoreLazy } from "@webpack"; 8 | 9 | import * as t from "./types/stores"; 10 | 11 | export const ApplicationStreamingStore: t.ApplicationStreamingStore = findStoreLazy("ApplicationStreamingStore"); 12 | export const RunningGameStore: t.RunningGameStore = findStoreLazy("RunningGameStore"); 13 | export const QuestsStore: t.QuestsStore = findByPropsLazy("getQuest"); 14 | export const ChannelStore: t.ChannelStore = findStoreLazy("ChannelStore"); 15 | export const GuildChannelStore: t.GuildChannelStore = findStoreLazy("GuildChannelStore"); 16 | -------------------------------------------------------------------------------- /settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { definePluginSettings } from "@api/Settings"; 8 | import { OptionType } from "@utils/types"; 9 | 10 | export default definePluginSettings({ 11 | acceptQuestsAutomatically: { 12 | type: OptionType.BOOLEAN, 13 | description: "Whether to accept available quests automatically.", 14 | default: true 15 | }, 16 | showQuestsButtonTopBar: { 17 | type: OptionType.BOOLEAN, 18 | description: "Whether to show the quests button in the top bar.", 19 | default: true, 20 | restartNeeded: true 21 | }, 22 | showQuestsButtonSettingsBar: { 23 | type: OptionType.BOOLEAN, 24 | description: "Whether to show the quests button in the settings bar.", 25 | default: false, 26 | restartNeeded: true 27 | }, 28 | showQuestsButtonBadges: { 29 | type: OptionType.BOOLEAN, 30 | description: "Whether to show badges on the quests button.", 31 | default: true 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /types/guildchannelstore.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | interface GuildData { 8 | "4": _4[]; 9 | id: string; 10 | SELECTABLE: SELECTABLE[]; 11 | VOCAL: VOCAL[]; 12 | count: number; 13 | } 14 | 15 | interface VOCAL { 16 | channel: Channel3; 17 | comparator: number; 18 | } 19 | 20 | interface Channel3 { 21 | bitrate_: number; 22 | flags_: number; 23 | guild_id: string; 24 | iconEmoji: IconEmoji; 25 | id: string; 26 | lastMessageId: string; 27 | name: string; 28 | nsfw_: boolean; 29 | parent_id: string; 30 | permissionOverwrites_: PermissionOverwrites3; 31 | position_: number; 32 | rateLimitPerUser_: number; 33 | rtcRegion: null; 34 | type: number; 35 | userLimit_: number; 36 | } 37 | 38 | interface PermissionOverwrites3 { 39 | [key: string]: Permission; 40 | } 41 | 42 | interface SELECTABLE { 43 | channel: Channel2; 44 | comparator: number; 45 | } 46 | 47 | interface Channel2 { 48 | flags_: number; 49 | guild_id: string; 50 | iconEmoji?: IconEmoji; 51 | id: string; 52 | lastMessageId: string; 53 | name: string; 54 | nsfw_: boolean; 55 | parent_id: string; 56 | permissionOverwrites_: PermissionOverwrites2; 57 | position_: number; 58 | rateLimitPerUser_: number; 59 | topic_: null | string; 60 | type: number; 61 | availableTags?: AvailableTag[]; 62 | defaultThreadRateLimitPerUser?: number; 63 | template?: string; 64 | defaultAutoArchiveDuration?: number; 65 | defaultReactionEmoji?: DefaultReactionEmoji; 66 | lastPinTimestamp?: string; 67 | themeColor?: null; 68 | } 69 | 70 | interface DefaultReactionEmoji { 71 | emojiId: null; 72 | emojiName: string; 73 | } 74 | 75 | interface AvailableTag { 76 | id: string; 77 | name: string; 78 | emojiId: null | string; 79 | emojiName: null | string; 80 | moderated: boolean; 81 | color: null; 82 | } 83 | 84 | interface PermissionOverwrites2 { 85 | [key: string]: Permission; 86 | } 87 | 88 | interface IconEmoji { 89 | id: null; 90 | name: string; 91 | } 92 | 93 | interface _4 { 94 | comparator: number; 95 | channel: Channel; 96 | } 97 | 98 | interface Channel { 99 | id: string; 100 | type: number; 101 | name: string; 102 | guild_id: null | string; 103 | permissionOverwrites_: PermissionOverwrites; 104 | flags_?: number; 105 | nsfw_?: boolean; 106 | position_?: number; 107 | rateLimitPerUser_?: number; 108 | } 109 | 110 | interface PermissionOverwrites { 111 | [key: string]: Permission; 112 | } 113 | 114 | interface Permission { 115 | id: string; 116 | type: number; 117 | allow: string; 118 | deny: string; 119 | } 120 | -------------------------------------------------------------------------------- /types/queststore.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | interface QuestData { 8 | key: string; 9 | value: QuestValue; 10 | } 11 | 12 | interface QuestValue { 13 | id: string; 14 | preview: boolean; 15 | config: Config; 16 | userStatus: UserStatus; 17 | targetedContent: any[]; 18 | } 19 | 20 | interface UserStatus { 21 | userId: string; 22 | questId: string; 23 | enrolledAt: string; 24 | completedAt: string; 25 | claimedAt: string; 26 | claimedTier: null; 27 | lastStreamHeartbeatAt: null; 28 | streamProgressSeconds: number; 29 | dismissedQuestContent: number; 30 | progress: Progress; 31 | } 32 | 33 | interface Progress { 34 | PLAY_ON_DESKTOP: PLAYONDESKTOP2; 35 | } 36 | 37 | interface PLAYONDESKTOP2 { 38 | eventName: string; 39 | value: number; 40 | updatedAt: string; 41 | completedAt: string; 42 | heartbeat: Heartbeat; 43 | } 44 | 45 | interface Heartbeat { 46 | lastBeatAt: string; 47 | expiresAt: null; 48 | } 49 | 50 | interface Config { 51 | id: string; 52 | configVersion: number; 53 | startsAt: string; 54 | expiresAt: string; 55 | features: number[]; 56 | application: Application; 57 | assets: Assets; 58 | colors: Colors; 59 | messages: Messages; 60 | taskConfigV2: TaskConfigV2; 61 | rewardsConfig: RewardsConfig; 62 | sharePolicy: string; 63 | } 64 | 65 | interface RewardsConfig { 66 | assignmentMethod: number; 67 | rewards: Reward[]; 68 | rewardsExpireAt: string; 69 | platforms: number[]; 70 | } 71 | 72 | interface Reward { 73 | type: number; 74 | skuId: string; 75 | messages: Messages2; 76 | orbQuantity: number; 77 | } 78 | 79 | interface Messages2 { 80 | redemptionInstructionsByPlatform: RedemptionInstructionsByPlatform; 81 | name: string; 82 | nameWithArticle: string; 83 | } 84 | 85 | interface RedemptionInstructionsByPlatform { 86 | "0": string; 87 | } 88 | 89 | interface TaskConfigV2 { 90 | tasks: Tasks; 91 | joinOperator: string; 92 | } 93 | 94 | interface Tasks { 95 | PLAY_ON_DESKTOP: PLAYONDESKTOP; 96 | } 97 | 98 | interface PLAYONDESKTOP { 99 | type: string; 100 | target: number; 101 | } 102 | 103 | interface Messages { 104 | questName: string; 105 | gameTitle: string; 106 | gamePublisher: string; 107 | } 108 | 109 | interface Colors { 110 | primary: string; 111 | secondary: string; 112 | } 113 | 114 | interface Assets { 115 | hero: string; 116 | heroVideo: string; 117 | questBarHero: string; 118 | questBarHeroVideo: string; 119 | gameTile: string; 120 | logotype: string; 121 | } 122 | 123 | interface Application { 124 | id: string; 125 | name: string; 126 | link: string; 127 | } 128 | 129 | interface QuestAction { 130 | questContent: number; 131 | questContentCTA: string; 132 | questContentPosition?: number; 133 | questContentRowIndex?: number; 134 | sourceQuestContent: number; 135 | sourceQuestContentCTA?: string; 136 | } 137 | -------------------------------------------------------------------------------- /components/QuestButton.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | import "./QuestButton.css"; 7 | 8 | import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; 9 | import { Tooltip, useEffect, useState } from "@webpack/common"; 10 | 11 | import { Flex } from "@components/Flex"; 12 | import { QuestsStore } from "../stores"; 13 | 14 | const QuestIcon = findByCodeLazy("\"M7.5 21.7a8.95"); 15 | const { navigateToQuestHome } = findByPropsLazy("navigateToQuestHome"); 16 | const TopBarButton = findComponentByCodeLazy("badgePosition"); 17 | const SettingsBarButton = findComponentByCodeLazy("iconForeground:"); 18 | const CountBadge = findComponentByCodeLazy("\"renderBadgeCount\""); 19 | 20 | function questsStatus() { 21 | const availableQuests = [...QuestsStore.quests.values()]; 22 | return availableQuests.reduce((acc, x) => { 23 | if (new Date(x.config.expiresAt).getTime() < Date.now()) { 24 | acc.expired++; 25 | } else if (x.userStatus?.claimedAt) { 26 | acc.claimed++; 27 | } else if (x.userStatus?.completedAt) { 28 | acc.claimable++; 29 | } else if (x.userStatus?.enrolledAt) { 30 | acc.enrolled++; 31 | } else { 32 | acc.enrollable++; 33 | } 34 | return acc; 35 | }, { enrollable: 0, enrolled: 0, claimable: 0, claimed: 0, expired: 0 }); 36 | } 37 | 38 | export function QuestsCount() { 39 | const [status, setStatus] = useState(questsStatus()); 40 | 41 | const checkForNewQuests = () => { 42 | setStatus(questsStatus()); 43 | }; 44 | 45 | useEffect(() => { 46 | QuestsStore.addChangeListener(checkForNewQuests); 47 | return () => { 48 | QuestsStore.removeChangeListener(checkForNewQuests); 49 | }; 50 | }, []); 51 | 52 | return ( 53 | 54 | {status.enrollable > 0 && ( 55 | 56 | {({ onMouseEnter, onMouseLeave }) => ( 57 | 63 | )} 64 | 65 | )} 66 | {status.enrolled > 0 && ( 67 | 68 | {({ onMouseEnter, onMouseLeave }) => ( 69 | 75 | )} 76 | 77 | )} 78 | {status.claimable > 0 && ( 79 | 80 | {({ onMouseEnter, onMouseLeave }) => ( 81 | 87 | )} 88 | 89 | )} 90 | {status.claimed > 0 && ( 91 | 92 | {({ onMouseEnter, onMouseLeave }) => ( 93 | 99 | )} 100 | 101 | )} 102 | 103 | ); 104 | } 105 | 106 | export function QuestButton({ type }: { type: "top-bar" | "settings-bar"; }) { 107 | const [state, setState] = useState(questsStatus()); 108 | 109 | const checkForNewQuests = () => { 110 | setState(questsStatus()); 111 | }; 112 | 113 | useEffect(() => { 114 | QuestsStore.addChangeListener(checkForNewQuests); 115 | return () => { 116 | QuestsStore.removeChangeListener(checkForNewQuests); 117 | }; 118 | }, []); 119 | 120 | const className = state.enrollable ? "quest-button-enrollable" : state.enrolled ? "quest-button-enrolled" : state.claimable ? "quest-button-claimable" : ""; 121 | const tooltip = state.enrollable ? `${state.enrollable} Enrollable Quests` : state.enrolled ? `${state.enrolled} Enrolled Quests` : state.claimable ? `${state.claimable} Claimable Quests` : "Quests"; 122 | if (type === "top-bar") { 123 | return ( 124 | 0 || state.enrolled > 0 || state.claimable > 0} 129 | badgePosition={"bottom"} 130 | icon={QuestIcon} 131 | iconSize={20} 132 | onClick={navigateToQuestHome} 133 | onContextMenu={undefined} 134 | tooltip={tooltip} 135 | tooltipPosition={"bottom"} 136 | hideOnClick={false} 137 | /> 138 | ); 139 | } else if (type === "settings-bar") { 140 | return ( 141 | 0 || state.enrolled > 0 || state.claimable > 0} 153 | badgePosition={"bottom"} 154 | icon={QuestIcon} 155 | iconSize={20} 156 | onClick={navigateToQuestHome} 157 | onContextMenu={undefined} 158 | hideOnClick={false} 159 | /> 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import definePlugin from "@utils/types"; 8 | import { findByCodeLazy, findByPropsLazy } from "@webpack"; 9 | import { FluxDispatcher, RestAPI } from "@webpack/common"; 10 | 11 | import { QuestButton, QuestsCount } from "./components/QuestButton"; 12 | import settings from "./settings"; 13 | import { ChannelStore, GuildChannelStore, QuestsStore, RunningGameStore } from "./stores"; 14 | 15 | const QuestApplyAction = findByCodeLazy("type:\"QUESTS_ENROLL_BEGIN\"") as (questId: string, action: QuestAction) => Promise; 16 | const QuestLocationMap = findByPropsLazy("QUEST_HOME_DESKTOP", "11") as Record; 17 | 18 | let availableQuests: QuestValue[] = []; 19 | let acceptableQuests: QuestValue[] = []; 20 | let completableQuests: QuestValue[] = []; 21 | 22 | const completingQuest = new Map(); 23 | const fakeGames = new Map(); 24 | const fakeApplications = new Map(); 25 | 26 | export default definePlugin({ 27 | name: "CompleteDiscordQuest", 28 | description: "A plugin that completes multiple discord quests in background simultaneously.", 29 | authors: [{ 30 | name: "nicola02nb", 31 | id: 257900031351193600n 32 | }], 33 | settings, 34 | patches: [ 35 | { 36 | find: ".winButtonsWithDivider]", 37 | replacement: { 38 | match: /(\((\i)\){)(let{leading)/, 39 | replace: "$1$2?.trailing?.props?.children?.unshift($self.renderQuestButtonTopBar());$3" 40 | } 41 | }, 42 | { 43 | find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}", 44 | replacement: { 45 | match: /className:\i\.buttons,.+?children:\[/, 46 | replace: "$&$self.renderQuestButtonSettingsBar()," 47 | } 48 | }, 49 | { // PTB Experimental 50 | find: "\"innerRef\",\"navigate\",\"onClick\"", 51 | replacement: { 52 | match: /(\i).createElement\("a",(\i)\)/, 53 | replace: "$1.createElement(\"a\",$self.renderQuestButtonBadges($2))" 54 | } 55 | }, 56 | { 57 | find: "location:\"GlobalDiscoverySidebar\"", 58 | replacement: { 59 | match: /(\(\i\){let{tab:(\i)}=.+?children:\i}\))(]}\))/, 60 | replace: "$1,$self.renderQuestButtonBadges($2)$3" 61 | } 62 | }, 63 | { 64 | find: "\"RunningGameStore\"", 65 | group: true, 66 | replacement: [ 67 | { 68 | match: /}getRunningGames\(\){return/, 69 | replace: "}getRunningGames(){const games=$self.getRunningGames();return games ? games : " 70 | }, 71 | { 72 | match: /}getGameForPID\((\i)\){/, 73 | replace: "}getGameForPID($1){const pid=$self.getGameForPID($1);if(pid){return pid;}" 74 | } 75 | ] 76 | }, 77 | { 78 | find: "ApplicationStreamingStore", 79 | replacement: { 80 | match: /}getStreamerActiveStreamMetadata\(\){/, 81 | replace: "}getStreamerActiveStreamMetadata(){const metadata=$self.getStreamerActiveStreamMetadata();if(metadata){return metadata;}" 82 | } 83 | } 84 | ], 85 | start: () => { 86 | QuestsStore.addChangeListener(updateQuests); 87 | updateQuests(); 88 | }, 89 | stop: () => { 90 | QuestsStore.removeChangeListener(updateQuests); 91 | stopCompletingAll(); 92 | }, 93 | 94 | renderQuestButtonTopBar() { 95 | if (settings.store.showQuestsButtonTopBar) { 96 | return ; 97 | } 98 | }, 99 | 100 | renderQuestButtonSettingsBar() { 101 | if (settings.store.showQuestsButtonSettingsBar) { 102 | return ; 103 | } 104 | }, 105 | 106 | renderQuestButtonBadges(questButton) { 107 | if (settings.store.showQuestsButtonBadges && typeof questButton === "string" && questButton === "quests") { 108 | return (); 109 | } 110 | // Experiment 111 | if (settings.store.showQuestsButtonBadges && questButton?.href?.startsWith("/quest-home") 112 | && Array.isArray(questButton?.children) && questButton.children.findIndex(child => child?.type === QuestsCount) === -1) { 113 | questButton.children.push(); 114 | } 115 | return questButton; 116 | }, 117 | 118 | getRunningGames() { 119 | if (fakeGames.size > 0) { 120 | return Array.from(fakeGames.values()); 121 | } 122 | }, 123 | 124 | getGameForPID(pid) { 125 | if (fakeGames.size > 0) { 126 | return Array.from(fakeGames.values()).find(game => game.pid === pid); 127 | } 128 | }, 129 | 130 | getStreamerActiveStreamMetadata() { 131 | if (fakeApplications.size > 0) { 132 | return Array.from(fakeApplications.values()).at(0); 133 | } 134 | } 135 | }); 136 | 137 | function updateQuests() { 138 | availableQuests = [...QuestsStore.quests.values()]; 139 | acceptableQuests = availableQuests.filter(x => x.userStatus?.enrolledAt == null && new Date(x.config.expiresAt).getTime() > Date.now()) || []; 140 | completableQuests = availableQuests.filter(x => x.userStatus?.enrolledAt && !x.userStatus?.completedAt && new Date(x.config.expiresAt).getTime() > Date.now()) || []; 141 | for (const quest of acceptableQuests) { 142 | acceptQuest(quest); 143 | } 144 | for (const quest of completableQuests) { 145 | if (completingQuest.has(quest.id)) { 146 | if (completingQuest.get(quest.id) === false) { 147 | completingQuest.delete(quest.id); 148 | } 149 | } else { 150 | completeQuest(quest); 151 | } 152 | } 153 | /* console.log("Available quests updated:", availableQuests); 154 | console.log("Acceptable quests updated:", acceptableQuests); 155 | console.log("Completable quests updated:", completableQuests); */ 156 | } 157 | 158 | function acceptQuest(quest: QuestValue) { 159 | if (!settings.store.acceptQuestsAutomatically) return; 160 | const action: QuestAction = { 161 | questContent: QuestLocationMap.QUEST_HOME_DESKTOP, 162 | questContentCTA: "ACCEPT_QUEST", 163 | sourceQuestContent: 0, 164 | }; 165 | QuestApplyAction(quest.id, action).then(() => { 166 | console.log("Accepted quest:", quest.config.messages.questName); 167 | }).catch(err => { 168 | console.error("Failed to accept quest:", quest.config.messages.questName, err); 169 | }); 170 | } 171 | 172 | function stopCompletingAll() { 173 | for (const quest of completableQuests) { 174 | if (completingQuest.has(quest.id)) { 175 | completingQuest.set(quest.id, false); 176 | } 177 | } 178 | console.log("Stopped completing all quests."); 179 | } 180 | 181 | function completeQuest(quest) { 182 | const isApp = typeof DiscordNative !== "undefined"; 183 | if (!quest) { 184 | console.log("You don't have any uncompleted quests!"); 185 | } else { 186 | const pid = Math.floor(Math.random() * 30000) + 1000; 187 | 188 | const applicationId = quest.config.application.id; 189 | const applicationName = quest.config.application.name; 190 | const { questName } = quest.config.messages; 191 | const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2; 192 | const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"].find(x => taskConfig.tasks[x] != null); 193 | if (!taskName) { 194 | console.log("Unknown task type for quest:", questName); 195 | return; 196 | } 197 | const secondsNeeded = taskConfig.tasks[taskName].target; 198 | let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; 199 | 200 | if (!isApp && taskName !== "WATCH_VIDEO" && taskName !== "WATCH_VIDEO_ON_MOBILE") { 201 | console.log("This no longer works in browser for non-video quests (" + taskName + "). Use the discord desktop app to complete the", questName, "quest!"); 202 | return; 203 | } 204 | 205 | completingQuest.set(quest.id, true); 206 | 207 | console.log(`Completing quest ${questName} (${quest.id}) - ${taskName} for ${secondsNeeded} seconds.`); 208 | 209 | switch (taskName) { 210 | case "WATCH_VIDEO": 211 | case "WATCH_VIDEO_ON_MOBILE": 212 | const maxFuture = 10, speed = 7, interval = 1; 213 | const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime(); 214 | let completed = false; 215 | const watchVideo = async () => { 216 | while (true) { 217 | const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture; 218 | const diff = maxAllowed - secondsDone; 219 | const timestamp = secondsDone + speed; 220 | 221 | if (!completingQuest.get(quest.id)) { 222 | console.log("Stopping completing quest:", questName); 223 | completingQuest.set(quest.id, false); 224 | break; 225 | } 226 | 227 | if (diff >= speed) { 228 | const res = await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } }); 229 | completed = res.body.completed_at != null; 230 | secondsDone = Math.min(secondsNeeded, timestamp); 231 | } 232 | 233 | if (timestamp >= secondsNeeded) { 234 | completingQuest.set(quest.id, false); 235 | break; 236 | } 237 | await new Promise(resolve => setTimeout(resolve, interval * 1000)); 238 | } 239 | if (!completed) { 240 | await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } }); 241 | } 242 | console.log("Quest completed!"); 243 | }; 244 | watchVideo(); 245 | console.log(`Spoofing video for ${questName}.`); 246 | break; 247 | 248 | case "PLAY_ON_DESKTOP": 249 | RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => { 250 | const appData = res.body[0]; 251 | const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", ""); 252 | 253 | const fakeGame = { 254 | cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, 255 | exeName, 256 | exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, 257 | hidden: false, 258 | isLauncher: false, 259 | id: applicationId, 260 | name: appData.name, 261 | pid: pid, 262 | pidPath: [pid], 263 | processName: appData.name, 264 | start: Date.now(), 265 | }; 266 | const realGames = fakeGames.size === 0 ? RunningGameStore.getRunningGames() : []; 267 | fakeGames.set(quest.id, fakeGame); 268 | const fakeGames2 = Array.from(fakeGames.values()); 269 | FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames2 }); 270 | 271 | const playOnDesktop = event => { 272 | if (event.questId !== quest.id) return; 273 | const progress = quest.config.configVersion === 1 ? event.userStatus.streamProgressSeconds : Math.floor(event.userStatus.progress.PLAY_ON_DESKTOP.value); 274 | console.log(`Quest progress ${questName}: ${progress}/${secondsNeeded}`); 275 | 276 | if (!completingQuest.get(quest.id) || progress >= secondsNeeded) { 277 | console.log("Stopping completing quest:", questName); 278 | 279 | fakeGames.delete(quest.id); 280 | const games = RunningGameStore.getRunningGames(); 281 | const added = fakeGames.size === 0 ? games : []; 282 | FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: added, games: games }); 283 | FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", playOnDesktop); 284 | 285 | if (progress >= secondsNeeded) { 286 | console.log("Quest completed!"); 287 | completingQuest.set(quest.id, false); 288 | } 289 | } 290 | }; 291 | FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", playOnDesktop); 292 | 293 | console.log(`Spoofed your game to ${applicationName}. Wait for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`); 294 | }); 295 | break; 296 | 297 | case "STREAM_ON_DESKTOP": 298 | const fakeApp = { 299 | id: applicationId, 300 | name: `FakeApp ${applicationName} (CompleteDiscordQuest)`, 301 | pid: pid, 302 | sourceName: null, 303 | }; 304 | fakeApplications.set(quest.id, fakeApp); 305 | 306 | const streamOnDesktop = event => { 307 | if (event.questId !== quest.id) return; 308 | const progress = quest.config.configVersion === 1 ? event.userStatus.streamProgressSeconds : Math.floor(event.userStatus.progress.STREAM_ON_DESKTOP.value); 309 | console.log(`Quest progress ${questName}: ${progress}/${secondsNeeded}`); 310 | 311 | if (!completingQuest.get(quest.id) || progress >= secondsNeeded) { 312 | console.log("Stopping completing quest:", questName); 313 | 314 | fakeApplications.delete(quest.id); 315 | FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", streamOnDesktop); 316 | 317 | if (progress >= secondsNeeded) { 318 | console.log("Quest completed!"); 319 | completingQuest.set(quest.id, false); 320 | } 321 | } 322 | }; 323 | FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", streamOnDesktop); 324 | 325 | console.log(`Spoofed your stream to ${applicationName}. Stream any window in vc for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`); 326 | console.log("Remember that you need at least 1 other person to be in the vc!"); 327 | break; 328 | 329 | case "PLAY_ACTIVITY": 330 | const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id; 331 | const streamKey = `call:${channelId}:1`; 332 | 333 | const playActivity = async () => { 334 | console.log("Completing quest", questName, "-", quest.config.messages.questName); 335 | 336 | while (true) { 337 | const res = await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: false } }); 338 | const progress = res.body.progress.PLAY_ACTIVITY.value; 339 | console.log(`Quest progress ${questName}: ${progress}/${secondsNeeded}`); 340 | 341 | await new Promise(resolve => setTimeout(resolve, 20 * 1000)); 342 | 343 | if (!completingQuest.get(quest.id) || progress >= secondsNeeded) { 344 | console.log("Stopping completing quest:", questName); 345 | 346 | if (progress >= secondsNeeded) { 347 | await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: true } }); 348 | console.log("Quest completed!"); 349 | completingQuest.set(quest.id, false); 350 | } 351 | break; 352 | } 353 | } 354 | }; 355 | playActivity(); 356 | break; 357 | 358 | default: 359 | console.error("Unknown task type:", taskName); 360 | completingQuest.set(quest.id, false); 361 | break; 362 | } 363 | } 364 | } 365 | --------------------------------------------------------------------------------