Last active
March 17, 2026 00:31
-
-
Save Ap0dexMe0/1ae689a076096f0a512b9605bd365428 to your computer and use it in GitHub Desktop.
Automation complete recent Discord Quest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function() { | |
| 'use strict'; | |
| class DiscordAutoQuest { | |
| constructor() { | |
| this.ELECTRON_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Discord/1.0.0 Chrome/120.0.0.0 Electron/28.0.0 Safari/537.36'; | |
| this.HEARTBEAT_INTERVAL = 30000; | |
| this.VIDEO_INTERVAL = 3000; | |
| this.WEBPACK_MAX_ATTEMPTS = 100; | |
| this.WEBPACK_CHECK_INTERVAL = 100; | |
| this.SUPPORTED_TASKS = new Set([ | |
| 'WATCH_VIDEO', | |
| 'PLAY_ON_DESKTOP', | |
| 'STREAM_ON_DESKTOP', | |
| 'PLAY_ACTIVITY', | |
| 'WATCH_VIDEO_ON_MOBILE' | |
| ]); | |
| this.STYLES = { | |
| header: 'color: #5865F2; font-weight: bold; font-size: 14px;', | |
| subheader: 'color: #57F287; font-weight: bold;', | |
| success: 'color: #57F287; font-weight: bold;', | |
| warning: 'color: #FEE75C; font-weight: bold;', | |
| error: 'color: #ED4245; font-weight: bold;', | |
| info: 'color: #5865F2;', | |
| quest: 'color: #EB459E; font-weight: bold;', | |
| questProgress: 'color: #5865F2; font-weight: bold;', | |
| questComplete: 'color: #57F287; font-weight: bold;', | |
| questVideo: 'color: #EB459E;', | |
| questHeartbeat: 'color: #57F287;', | |
| progressBar: 'color: #EB459E; font-weight: bold;', | |
| progressComplete: 'color: #57F287; font-weight: bold;', | |
| technical: 'color: #99AAB5;', | |
| webpack: 'color: #FEE75C;', | |
| time: 'color: #5865F2;', | |
| waiting: 'color: #FEE75C;' | |
| }; | |
| this.stores = null; | |
| this.isRunning = false; | |
| this.activeQuests = new Map(); | |
| this.questStatusCache = new Map(); | |
| this.videoHandlers = new Map(); | |
| this.init(); | |
| } | |
| init() { | |
| this.printHeader(); | |
| this.log('Initializing Discord Auto Quest...', 'info'); | |
| try { | |
| this.overrideUserAgent(); | |
| this.waitForWebpack(); | |
| } catch (error) { | |
| this.log(`Initialization failed: ${error.message}`, 'error', error); | |
| } | |
| } | |
| printHeader() { | |
| console.log( | |
| `%c╔════════════════════════════════════════╗\n` + | |
| `%c║ ║\n` + | |
| `%c║ %cDiscord Auto Quest %c ║\n` + | |
| `%c║ %cAutomated Quest Completer %c ║\n` + | |
| `%c║ ║\n` + | |
| `%c╚════════════════════════════════════════╝`, | |
| this.STYLES.header, this.STYLES.header, | |
| this.STYLES.header, this.STYLES.success, this.STYLES.header, | |
| this.STYLES.header, this.STYLES.info, this.STYLES.header, | |
| this.STYLES.header, this.STYLES.header | |
| ); | |
| } | |
| log(message, style = 'info', data = null) { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| const prefix = `%c[${timestamp}]%c [Discord Auto Quest]`; | |
| const mainStyle = this.STYLES.time; | |
| const messageStyle = this.STYLES[style] || this.STYLES.info; | |
| if (data) { | |
| console.log(`${prefix} ${message}`, mainStyle, messageStyle, data); | |
| } else { | |
| console.log(`${prefix} ${message}`, mainStyle, messageStyle); | |
| } | |
| } | |
| logQuest(questName, message, type = 'quest') { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| const prefix = `%c[${timestamp}]%c [Quest]`; | |
| const questStyle = this.STYLES[type]; | |
| const timeStyle = this.STYLES.time; | |
| console.log(`${prefix} %c"${questName}"%c: ${message}`, | |
| timeStyle, this.STYLES.info, questStyle, this.STYLES.info); | |
| } | |
| logProgress(questName, current, total, type = 'progress') { | |
| const percent = Math.min(100, Math.floor((current / total) * 100)); | |
| const barLength = 20; | |
| const filled = Math.floor((percent / 100) * barLength); | |
| const empty = barLength - filled; | |
| const bar = '█'.repeat(filled) + '░'.repeat(empty); | |
| const timestamp = new Date().toLocaleTimeString(); | |
| const prefix = `%c[${timestamp}]%c [Progress]`; | |
| console.log( | |
| `${prefix} %c"${questName}"%c: [%c${bar}%c] %c${Math.round(current)}/${Math.round(total)}s %c(${percent}%)`, | |
| this.STYLES.time, this.STYLES.info, | |
| this.STYLES.quest, | |
| this.STYLES.info, | |
| this.STYLES.progressBar, | |
| this.STYLES.info, | |
| this.STYLES.questProgress, | |
| percent === 100 ? this.STYLES.progressComplete : this.STYLES.info | |
| ); | |
| } | |
| overrideUserAgent() { | |
| const descriptors = { | |
| userAgent: { value: this.ELECTRON_USER_AGENT }, | |
| platform: { value: 'Win32' }, | |
| appVersion: { value: this.ELECTRON_USER_AGENT } | |
| }; | |
| let overridden = 0; | |
| Object.entries(descriptors).forEach(([prop, descriptor]) => { | |
| try { | |
| Object.defineProperty(navigator, prop, { | |
| get: () => descriptor.value, | |
| configurable: true | |
| }); | |
| overridden++; | |
| } catch (error) { | |
| this.log(`Failed to override ${prop}`, 'warning'); | |
| } | |
| }); | |
| if (overridden > 0) { | |
| this.log(`User-Agent override active (${overridden}/3 properties)`, 'success'); | |
| } | |
| } | |
| waitForWebpack() { | |
| let attempts = 0; | |
| this.log('Waiting for Discord webpack modules...', 'webpack'); | |
| const check = () => { | |
| if (attempts >= this.WEBPACK_MAX_ATTEMPTS) { | |
| this.log('Webpack not found after maximum attempts', 'error'); | |
| return; | |
| } | |
| if (typeof window.webpackChunkdiscord_app === 'undefined') { | |
| attempts++; | |
| if (attempts % 10 === 0) { | |
| this.log(`Still waiting for webpack... (attempt ${attempts}/${this.WEBPACK_MAX_ATTEMPTS})`, 'waiting'); | |
| } | |
| setTimeout(check, this.WEBPACK_CHECK_INTERVAL); | |
| return; | |
| } | |
| try { | |
| this.loadWebpackModules(); | |
| } catch (error) { | |
| this.log(`Webpack loading attempt ${attempts + 1} failed: ${error.message}`, 'warning', error); | |
| attempts++; | |
| setTimeout(check, this.WEBPACK_CHECK_INTERVAL); | |
| } | |
| }; | |
| check(); | |
| } | |
| loadWebpackModules() { | |
| const originalJQuery = window.$; | |
| delete window.$; | |
| try { | |
| const webpackRequire = window.webpackChunkdiscord_app.push([ | |
| [Symbol()], | |
| {}, | |
| (require) => require | |
| ]); | |
| window.webpackChunkdiscord_app.pop(); | |
| if (!webpackRequire?.c || Object.keys(webpackRequire.c).length < 10) { | |
| throw new Error('Insufficient webpack modules loaded'); | |
| } | |
| const moduleCount = Object.keys(webpackRequire.c).length; | |
| this.log(`Webpack loaded successfully with ${moduleCount} modules`, 'webpack'); | |
| this.stores = this.loadStores(webpackRequire); | |
| if (this.stores) { | |
| this.log('All required stores loaded successfully', 'success'); | |
| this.startQuestAutomation(); | |
| } | |
| } finally { | |
| if (originalJQuery) { | |
| window.$ = originalJQuery; | |
| } | |
| } | |
| } | |
| findModule(webpackRequire, predicate) { | |
| const modules = Object.values(webpackRequire.c); | |
| for (const module of modules) { | |
| if (!module?.exports) continue; | |
| const exports = module.exports; | |
| const candidates = [ | |
| exports.A, | |
| exports.Ay, | |
| exports, | |
| exports.default | |
| ].filter(Boolean); | |
| for (const candidate of candidates) { | |
| if (predicate(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| loadStores(webpackRequire) { | |
| const requiredStores = [ | |
| { name: 'ApplicationStreamingStore', predicate: m => m.__proto__?.getStreamerActiveStreamMetadata }, | |
| { name: 'RunningGameStore', predicate: m => m.getRunningGames }, | |
| { name: 'QuestsStore', predicate: m => m.__proto__?.getQuest }, | |
| { name: 'ChannelStore', predicate: m => m.__proto__?.getAllThreadsForParent }, | |
| { name: 'GuildChannelStore', predicate: m => m.getSFWDefaultChannel }, | |
| { name: 'FluxDispatcher', predicate: m => m.h?.__proto__?.flushWaitQueue }, | |
| { name: 'api', predicate: m => m.Bo?.get } | |
| ]; | |
| const stores = {}; | |
| const loaded = []; | |
| const missing = []; | |
| requiredStores.forEach(({ name, predicate }) => { | |
| let store = this.findModule(webpackRequire, predicate); | |
| if (name === 'FluxDispatcher' && store?.h) { | |
| store = store.h; | |
| } | |
| if (name === 'api' && store?.Bo) { | |
| store = store.Bo; | |
| } | |
| if (store) { | |
| stores[name] = store; | |
| loaded.push(name); | |
| } else { | |
| missing.push(name); | |
| } | |
| }); | |
| if (missing.length > 0) { | |
| this.log(`Missing critical stores: ${missing.join(', ')}`, 'error'); | |
| this.log(`Loaded stores: ${loaded.join(', ')}`, 'technical'); | |
| throw new Error(`Missing stores: ${missing.join(', ')}`); | |
| } | |
| this.log(`Loaded ${loaded.length} stores successfully`, 'success'); | |
| loaded.forEach(storeName => { | |
| this.log(`✓ ${storeName}`, 'technical'); | |
| }); | |
| return stores; | |
| } | |
| async startQuestAutomation() { | |
| if (this.isRunning) { | |
| this.log('Automation already running', 'warning'); | |
| return; | |
| } | |
| this.isRunning = true; | |
| this.log('Starting quest automation...', 'subheader'); | |
| try { | |
| this.setupVideoHandlers(); | |
| const quests = this.getActiveQuests(); | |
| if (quests.length === 0) { | |
| this.log('No active quests found', 'warning'); | |
| this.log('Make sure you have enrolled in quests and they are not completed or expired', 'info'); | |
| this.isRunning = false; | |
| return; | |
| } | |
| this.log(`Found ${quests.length} active quest(s)`, 'success'); | |
| const questStates = quests.map(quest => this.createQuestState(quest)); | |
| this.activeQuests = new Map(questStates.map(state => [state.quest.id, state])); | |
| questStates.forEach(state => { | |
| const typeColor = state.taskType.startsWith('WATCH_VIDEO') ? | |
| this.STYLES.questVideo : this.STYLES.questHeartbeat; | |
| console.log( | |
| `%c•%c ${state.questName} %c(${state.taskType})%c - ${state.appName}`, | |
| this.STYLES.success, | |
| this.STYLES.quest, | |
| typeColor, | |
| this.STYLES.info | |
| ); | |
| }); | |
| await this.processQuests(questStates); | |
| } catch (error) { | |
| this.log(`Automation failed: ${error.message}`, 'error', error); | |
| this.isRunning = false; | |
| } | |
| } | |
| setupVideoHandlers() { | |
| const handleVideoPlayback = (videoElement) => { | |
| if (!videoElement || this.videoHandlers.has(videoElement)) return; | |
| const handler = { | |
| element: videoElement, | |
| isPlaying: false, | |
| retryCount: 0, | |
| maxRetries: 3, | |
| play: async () => { | |
| try { | |
| if (this.handler.isPlaying) return; | |
| // Set volume rendah untuk menghindari suara | |
| videoElement.volume = 0.01; | |
| videoElement.muted = true; | |
| const playPromise = videoElement.play(); | |
| if (playPromise !== undefined) { | |
| await playPromise; | |
| this.handler.isPlaying = true; | |
| this.handler.retryCount = 0; | |
| this.log('Video playback started successfully', 'technical'); | |
| } | |
| } catch (error) { | |
| this.handler.retryCount++; | |
| if (error.name === 'AbortError') { | |
| this.log('Video playback aborted, will retry...', 'warning'); | |
| if (this.handler.retryCount < this.handler.maxRetries) { | |
| setTimeout(() => this.handler.play(), 1000); | |
| } | |
| } else { | |
| this.log(`Video playback error: ${error.name}`, 'warning'); | |
| } | |
| } | |
| }, | |
| pause: () => { | |
| if (this.handler.isPlaying) { | |
| videoElement.pause(); | |
| this.handler.isPlaying = false; | |
| } | |
| } | |
| }; | |
| this.videoHandlers.set(videoElement, handler); | |
| videoElement.addEventListener('abort', () => { | |
| this.log('Video aborted by browser', 'warning'); | |
| handler.isPlaying = false; | |
| }); | |
| videoElement.addEventListener('error', () => { | |
| this.log('Video error occurred', 'warning'); | |
| handler.isPlaying = false; | |
| }); | |
| videoElement.addEventListener('loadeddata', () => { | |
| this.log('Video data loaded', 'technical'); | |
| }); | |
| }; | |
| const videos = document.querySelectorAll('video'); | |
| videos.forEach(handleVideoPlayback); | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| mutation.addedNodes.forEach((node) => { | |
| if (node.nodeName === 'VIDEO') { | |
| handleVideoPlayback(node); | |
| } else if (node.querySelectorAll) { | |
| node.querySelectorAll('video').forEach(handleVideoPlayback); | |
| } | |
| }); | |
| }); | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| this.videoObserver = observer; | |
| } | |
| getActiveQuests() { | |
| const { QuestsStore } = this.stores; | |
| return [...(QuestsStore.quests?.values() || [])].filter(quest => { | |
| if (!quest?.config || !quest.userStatus) return false; | |
| const isExpired = new Date(quest.config.expiresAt).getTime() <= Date.now(); | |
| const isCompleted = !!quest.userStatus.completedAt; | |
| const isEnrolled = !!quest.userStatus.enrolledAt; | |
| if (!isEnrolled || isCompleted || isExpired) return false; | |
| const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2; | |
| if (!taskConfig?.tasks) return false; | |
| return Array.from(this.SUPPORTED_TASKS).some(type => | |
| taskConfig.tasks[type] != null | |
| ); | |
| }); | |
| } | |
| createQuestState(quest) { | |
| const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2; | |
| const taskType = Array.from(this.SUPPORTED_TASKS).find( | |
| type => taskConfig.tasks[type] != null | |
| ); | |
| const taskData = taskConfig.tasks[taskType] || {}; | |
| const secondsNeeded = taskData.target || 0; | |
| const currentProgress = quest.userStatus?.progress?.[taskType]?.value | |
| ?? quest.userStatus?.streamProgressSeconds | |
| ?? 0; | |
| return { | |
| quest, | |
| taskType, | |
| secondsNeeded, | |
| currentProgress, | |
| completed: currentProgress >= secondsNeeded, | |
| lastUpdate: 0, | |
| enrolledAt: new Date(quest.userStatus.enrolledAt).getTime(), | |
| appName: quest.config.application?.name || 'Unknown', | |
| appId: quest.config.application?.id || '', | |
| questName: quest.config.messages?.questName || 'Unknown Quest', | |
| attempts: 0, | |
| maxAttempts: 3, | |
| lastError: null, | |
| safeMode: false | |
| }; | |
| } | |
| async processQuests(questStates) { | |
| const videoQuests = questStates.filter(s => | |
| s.taskType.startsWith('WATCH_VIDEO') && !s.completed | |
| ); | |
| const heartbeatQuests = questStates.filter(s => | |
| !s.taskType.startsWith('WATCH_VIDEO') && !s.completed | |
| ); | |
| this.log(`Processing ${videoQuests.length} video quest(s) and ${heartbeatQuests.length} heartbeat quest(s)`, 'subheader'); | |
| const promises = []; | |
| if (videoQuests.length > 0) { | |
| // Gunakan safe mode untuk video quests | |
| videoQuests.forEach(state => state.safeMode = true); | |
| promises.push(this.processVideoQuestsSafely(videoQuests)); | |
| } | |
| if (heartbeatQuests.length > 0) { | |
| promises.push(this.processHeartbeatQuests(heartbeatQuests)); | |
| } | |
| if (promises.length === 0) { | |
| this.log('All quests are already completed!', 'success'); | |
| this.isRunning = false; | |
| return; | |
| } | |
| try { | |
| await Promise.allSettled(promises); | |
| this.log('All quests have been processed!', 'success'); | |
| this.printCompletionMessage(); | |
| } catch (error) { | |
| this.log('Quest processing failed', 'error', error); | |
| } finally { | |
| this.isRunning = false; | |
| // Cleanup | |
| this.cleanupVideoHandlers(); | |
| } | |
| } | |
| async processVideoQuestsSafely(videoStates) { | |
| this.log(`Starting SAFE video quest processing...`, 'questVideo'); | |
| while (videoStates.some(state => !state.completed)) { | |
| const now = Date.now(); | |
| for (const state of videoStates) { | |
| if (state.completed) continue; | |
| if (now - state.lastUpdate >= this.VIDEO_INTERVAL) { | |
| await this.safeUpdateVideoProgress(state); | |
| state.lastUpdate = now; | |
| } | |
| } | |
| videoStates = videoStates.filter(state => { | |
| if (state.completed) { | |
| this.logQuest(state.questName, 'Video quest completed!', 'questComplete'); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| if (videoStates.length === 0) { | |
| break; | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| } | |
| } | |
| async safeUpdateVideoProgress(state) { | |
| if (state.attempts >= state.maxAttempts) { | |
| this.logQuest(state.questName, 'Max attempts reached, entering safe mode', 'warning'); | |
| state.safeMode = true; | |
| return; | |
| } | |
| const { quest, secondsNeeded, enrolledAt, currentProgress, questName } = state; | |
| const MAX_FUTURE = 15; | |
| const SAFE_INCREMENT = 5; | |
| const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + MAX_FUTURE; | |
| if (maxAllowed <= currentProgress) { | |
| this.logQuest(questName, `Waiting for time allowance (${currentProgress}/${maxAllowed}s)`, 'waiting'); | |
| return; | |
| } | |
| const remaining = secondsNeeded - currentProgress; | |
| const increment = Math.min(SAFE_INCREMENT, remaining); | |
| const nextProgress = Math.min( | |
| secondsNeeded, | |
| currentProgress + increment | |
| ); | |
| try { | |
| this.logQuest(questName, `Sending safe progress: ${Math.round(nextProgress)}/${secondsNeeded}s`, 'questVideo'); | |
| const response = await this.stores.api.post({ | |
| url: `/quests/${quest.id}/video-progress`, | |
| body: { timestamp: Math.round(nextProgress) }, | |
| timeout: 10000 | |
| }).catch(err => { | |
| throw new Error(`API Error: ${err.message}`); | |
| }); | |
| state.currentProgress = nextProgress; | |
| state.attempts = 0; | |
| state.lastError = null; | |
| if (response && response.body) { | |
| const serverProgress = response.body.progress?.[state.taskType]?.value ?? nextProgress; | |
| if (serverProgress > state.currentProgress) { | |
| state.currentProgress = serverProgress; | |
| this.logQuest(questName, `Server progress updated to: ${serverProgress}s`, 'technical'); | |
| } | |
| if (response.body.completed_at != null || state.currentProgress >= secondsNeeded) { | |
| await this.sendFinalUpdate(state); | |
| state.completed = true; | |
| return; | |
| } | |
| } | |
| this.logProgress(questName, state.currentProgress, secondsNeeded, 'questVideo'); | |
| } catch (error) { | |
| state.attempts++; | |
| state.lastError = error.message; | |
| if (error.message.includes('AbortError') || error.message.includes('play()')) { | |
| this.logQuest(questName, 'Video playback interrupted, will retry...', 'warning'); | |
| await new Promise(resolve => setTimeout(resolve, 3000)); | |
| } else { | |
| this.logQuest(questName, `Update failed: ${error.message}`, 'error'); | |
| if (error.status === 400 || error.status === 403 || error.status === 429) { | |
| await new Promise(resolve => setTimeout(resolve, 5000)); | |
| if (error.status === 429) { | |
| this.logQuest(questName, 'Rate limited, waiting 30 seconds...', 'warning'); | |
| await new Promise(resolve => setTimeout(resolve, 30000)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| async sendFinalUpdate(state) { | |
| try { | |
| await this.stores.api.post({ | |
| url: `/quests/${state.quest.id}/video-progress`, | |
| body: { timestamp: state.secondsNeeded } | |
| }); | |
| this.logQuest(state.questName, 'Final update sent successfully', 'success'); | |
| } catch (error) { | |
| this.logQuest(state.questName, 'Final update failed (ignored)', 'technical'); | |
| } | |
| } | |
| async processHeartbeatQuests(heartbeatStates) { | |
| this.log(`Starting heartbeat quest processing...`, 'questHeartbeat'); | |
| while (heartbeatStates.some(state => !state.completed)) { | |
| const now = Date.now(); | |
| for (const state of heartbeatStates) { | |
| if (state.completed) continue; | |
| if (now - state.lastUpdate >= this.HEARTBEAT_INTERVAL) { | |
| await this.sendHeartbeat(state); | |
| state.lastUpdate = now; | |
| } | |
| } | |
| heartbeatStates = heartbeatStates.filter(state => { | |
| if (state.completed) { | |
| this.logQuest(state.questName, 'Heartbeat quest completed!', 'questComplete'); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| if (heartbeatStates.length === 0) { | |
| break; | |
| } | |
| this.log(`Waiting ${Math.ceil(this.HEARTBEAT_INTERVAL / 1000)}s for next heartbeat...`, 'waiting'); | |
| await new Promise(resolve => setTimeout(resolve, 5000)); | |
| } | |
| } | |
| async sendHeartbeat(state) { | |
| if (state.attempts >= state.maxAttempts) { | |
| this.logQuest(state.questName, 'Max attempts reached, skipping', 'warning'); | |
| return; | |
| } | |
| const { quest, taskType, secondsNeeded, questName } = state; | |
| const { api, ChannelStore, GuildChannelStore } = this.stores; | |
| let streamKey = `call:${quest.id}:1`; | |
| if (taskType === 'PLAY_ACTIVITY') { | |
| const channelId = this.findVoiceChannelId(ChannelStore, GuildChannelStore); | |
| if (channelId) { | |
| streamKey = `call:${channelId}:1`; | |
| } else { | |
| this.logQuest(questName, 'No voice channel found for activity quest', 'warning'); | |
| return; | |
| } | |
| } | |
| try { | |
| this.logQuest(questName, 'Sending heartbeat...', 'questHeartbeat'); | |
| const response = await api.post({ | |
| url: `/quests/${quest.id}/heartbeat`, | |
| body: { | |
| stream_key: streamKey, | |
| terminal: state.currentProgress >= secondsNeeded - 5 | |
| } | |
| }); | |
| const serverProgress = response.body?.progress?.[taskType]?.value ?? state.currentProgress; | |
| state.currentProgress = serverProgress; | |
| state.attempts = 0; | |
| this.logProgress(questName, state.currentProgress, secondsNeeded, 'questHeartbeat'); | |
| if (state.currentProgress >= secondsNeeded || response.body?.completed_at != null) { | |
| try { | |
| await api.post({ | |
| url: `/quests/${quest.id}/heartbeat`, | |
| body: { stream_key: streamKey, terminal: true } | |
| }); | |
| } catch (e) { | |
| // Ignore | |
| } | |
| state.completed = true; | |
| return; | |
| } | |
| } catch (error) { | |
| state.attempts++; | |
| this.logQuest(questName, `Heartbeat failed: ${error.message}`, 'error'); | |
| if (error.status === 400 || error.status === 403 || error.status === 429) { | |
| this.logQuest(questName, 'API error, waiting before retry...', 'warning'); | |
| await new Promise(resolve => setTimeout(resolve, 10000)); | |
| } | |
| } | |
| } | |
| findVoiceChannelId(ChannelStore, GuildChannelStore) { | |
| const privateChannels = ChannelStore.getSortedPrivateChannels?.() || []; | |
| let channelId = privateChannels[0]?.id; | |
| if (!channelId) { | |
| const guilds = Object.values(GuildChannelStore.getAllGuilds?.() || {}); | |
| const guildWithVoice = guilds.find(guild => | |
| guild?.VOCAL?.length > 0 | |
| ); | |
| if (guildWithVoice) { | |
| channelId = guildWithVoice.VOCAL[0]?.channel?.id; | |
| } | |
| } | |
| return channelId; | |
| } | |
| cleanupVideoHandlers() { | |
| if (this.videoObserver) { | |
| this.videoObserver.disconnect(); | |
| } | |
| this.videoHandlers.forEach(handler => { | |
| if (handler.isPlaying) { | |
| handler.pause(); | |
| } | |
| }); | |
| this.videoHandlers.clear(); | |
| } | |
| printCompletionMessage() { | |
| console.log( | |
| `%c╔════════════════════════════════════════╗\n` + | |
| `%c║ ║\n` + | |
| `%c║ %c🎉 All Quests Complete! %c║\n` + | |
| `%c║ %c✅ Automation Finished %c ║\n` + | |
| `%c║ %c✨ Happy Questing! %c ║\n` + | |
| `%c║ ║\n` + | |
| `%c╚════════════════════════════════════════╝`, | |
| this.STYLES.success, this.STYLES.success, | |
| this.STYLES.success, this.STYLES.header, this.STYLES.success, | |
| this.STYLES.success, this.STYLES.success, this.STYLES.success, | |
| this.STYLES.success, this.STYLES.success, this.STYLES.success, | |
| this.STYLES.success, this.STYLES.success | |
| ); | |
| } | |
| } | |
| try { | |
| new DiscordAutoQuest(); | |
| } catch (error) { | |
| console.error('Fatal error starting Discord Auto Quest:', error); | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
perfect