Skip to content

Instantly share code, notes, and snippets.

@Rakise
Last active December 21, 2025 01:50
Show Gist options
  • Select an option

  • Save Rakise/517c617753bd0876086b59c66c447790 to your computer and use it in GitHub Desktop.

Select an option

Save Rakise/517c617753bd0876086b59c66c447790 to your computer and use it in GitHub Desktop.

Since the original creator aamiaa does not want new features or improvements added to their code, I created this gist with some quality-of-life improvements instead.

My version still fully respects the Discord API and does not bypass or abuse it.


Patch Notes

Patch 0.0

  • Added a simple and intuitive UI
  • Implemented quest queueing
  • Enabled multitasking between different quests

Patch 0.1

  • Added proper handling for HTTP 429 (rate limiting)
  • Added safeguards to prevent multitasking a lot when quests are of the same type

Patch 0.1.1

  • Added support for starting individual quests manually

How to use this script:

  1. Accept a quest under Discover -> Quests
  2. Press Ctrl+Shift+I to open DevTools
  3. Go to the Console tab
  4. Paste the following code and hit enter:
Click to expand
{
    // --- 0. Cleanup Previous Instances ---
    const existingUI = document.getElementById('autoquest-overlay');
    if (existingUI) existingUI.remove();
    const existingCanvas = document.getElementById('aq-confetti');
    if (existingCanvas) existingCanvas.remove();
    
    // --- 1. Discord Internals Setup ---
    delete window.$;
    let wpRequire = webpackChunkdiscord_app.push([[Symbol()], {}, r => r]);
    webpackChunkdiscord_app.pop();

    let ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getStreamerActiveStreamMetadata).exports.Z;
    let RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getRunningGames).exports.ZP;
    let QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getQuest).exports.Z;
    let ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.getAllThreadsForParent).exports.Z;
    let GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.ZP?.getSFWDefaultChannel).exports.ZP;
    let FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.Z?.__proto__?.flushWaitQueue).exports.Z;
    let api = Object.values(wpRequire.c).find(x => x?.exports?.tn?.get).exports.tn;

    const isApp = typeof DiscordNative !== "undefined";
    const SLEEP_BUFFER = 5000; 

    // --- 2. VFX: Confetti Engine ---
    const fireConfetti = (duration = 2000, amount = 100) => {
        const canvas = document.createElement('canvas');
        canvas.id = 'aq-confetti';
        canvas.style.position = 'fixed';
        canvas.style.top = '0';
        canvas.style.left = '0';
        canvas.style.width = '100vw';
        canvas.style.height = '100vh';
        canvas.style.pointerEvents = 'none';
        canvas.style.zIndex = '100000';
        document.body.appendChild(canvas);

        const ctx = canvas.getContext('2d');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        const particles = [];
        const colors = ['#5865F2', '#57F287', '#EB459E', '#FEE75C', '#ffffff'];

        for (let i = 0; i < amount; i++) {
            particles.push({
                x: Math.random() * canvas.width,
                y: Math.random() * canvas.height - canvas.height,
                vx: Math.random() * 4 - 2,
                vy: Math.random() * 4 + 2,
                color: colors[Math.floor(Math.random() * colors.length)],
                size: Math.random() * 8 + 4,
                rotation: Math.random() * 360
            });
        }

        let start = null;
        const animate = (timestamp) => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            particles.forEach(p => {
                p.x += p.vx;
                p.y += p.vy;
                p.rotation += 2;
                
                ctx.save();
                ctx.translate(p.x, p.y);
                ctx.rotate(p.rotation * Math.PI / 180);
                ctx.fillStyle = p.color;
                ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
                ctx.restore();

                if (p.y > canvas.height) p.y = -20;
            });

            if (progress < duration) {
                requestAnimationFrame(animate);
            } else {
                canvas.remove();
            }
        };
        requestAnimationFrame(animate);
    };

    // --- 3. UI Builder & State Management ---
    
    const colors = {
        bg: '#2b2d31', 
        header: '#1e1f22',
        primary: '#5865F2',
        success: '#23a559',
        text: '#dbdee1',
        subtext: '#949ba4',
        barBg: '#3f4147'
    };

    const styles = `
        #autoquest-overlay {
            position: fixed;
            top: 100px;
            left: 100px;
            width: 400px;
            height: auto;
            min-width: 300px;
            min-height: 100px;
            max-height: 80vh;
            background: rgba(43, 45, 49, 0.98);
            border-radius: 12px;
            box-shadow: 0 12px 32px rgba(0,0,0,0.4);
            font-family: 'gg sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
            color: ${colors.text};
            z-index: 99999;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            border: 1px solid rgba(255,255,255,0.08);
            backdrop-filter: blur(12px);
            transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), 
                        height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                        border-radius 0.3s,
                        opacity 0.2s,
                        transform 0.2s;
        }
        
        /* Minimized State */
        #autoquest-overlay.minimized {
            width: 60px !important;
            height: 60px !important;
            min-width: 0 !important; 
            min-height: 0 !important;
            border-radius: 50% !important;
            cursor: pointer;
            overflow: hidden;
            background: ${colors.primary};
            box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4);
            border: 2px solid rgba(255,255,255,0.2);
        }
        #autoquest-overlay.minimized #autoquest-header,
        #autoquest-overlay.minimized #autoquest-content,
        #autoquest-overlay.minimized #autoquest-footer,
        #autoquest-overlay.minimized .aq-resize-handle {
            display: none;
        }
        #autoquest-overlay.minimized::after {
            content: "AQ";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-weight: 900;
            font-size: 20px;
            pointer-events: none;
        }
        #autoquest-overlay.minimized:hover {
            transform: scale(1.1);
        }

        #autoquest-header {
            background: rgba(30, 31, 34, 0.8);
            padding: 14px 16px;
            font-weight: 700;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid rgba(255,255,255,0.05);
            cursor: default;
            user-select: none;
            flex-shrink: 0;
        }
        .header-controls { display: flex; gap: 8px; }
        
        #autoquest-content {
            padding: 16px;
            flex: 1;
            overflow-y: auto;
            min-height: 0; 
        }
        #autoquest-content::-webkit-scrollbar { width: 6px; }
        #autoquest-content::-webkit-scrollbar-track { background: transparent; }
        #autoquest-content::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.4); border-radius: 4px; }
        
        .quest-card {
            position: relative;
            background: rgba(30, 31, 34, 0.4);
            border-radius: 8px;
            padding: 16px;
            margin-bottom: 10px;
            border: 1px solid rgba(255,255,255,0.05);
            transition: transform 0.2s, border-color 0.2s;
            display: flex;
            gap: 12px;
            align-items: center;
            overflow: hidden;
        }
        .quest-card:hover {
            border-color: rgba(255,255,255,0.2);
            background: rgba(40, 42, 45, 0.6);
            transform: translateY(-2px);
        }
        
        .quest-info {
            flex: 1;
            min-width: 0; 
            z-index: 1;
        }
        .quest-actions {
            z-index: 2;
            flex-shrink: 0;
        }
        .quest-title {
            font-weight: 700;
            font-size: 15px;
            margin-bottom: 8px;
            color: #fff;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .quest-meta {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 8px;
        }
        .quest-type {
            font-size: 10px;
            color: #fff;
            text-transform: uppercase;
            font-weight: 800;
            letter-spacing: 0.5px;
            background: rgba(255,255,255,0.1);
            padding: 4px 8px;
            border-radius: 4px;
        }
        .quest-timer {
            font-size: 12px;
            font-family: 'Consolas', 'Monaco', monospace;
            color: ${colors.text};
            font-variant-numeric: tabular-nums;
            background: rgba(0,0,0,0.4);
            padding: 2px 6px;
            border-radius: 4px;
        }
        .progress-track {
            height: 8px;
            background: rgba(0,0,0,0.4);
            border-radius: 4px;
            overflow: hidden;
            margin-top: 8px;
        }
        .progress-fill {
            height: 100%;
            background: ${colors.primary};
            width: 0%;
            transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
            position: relative;
            overflow: hidden;
        }
        .progress-fill::after {
            content: '';
            position: absolute;
            top: 0; left: 0; bottom: 0; right: 0;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
            transform: translateX(-100%);
            animation: shimmer 2s infinite;
        }
        @keyframes shimmer { 100% { transform: translateX(100%); } }

        .status-text {
            font-size: 12px;
            color: #efefef;
            margin-top: 6px;
            display: flex;
            justify-content: space-between;
            font-weight: 500;
        }
        #autoquest-footer {
            padding: 16px;
            background: rgba(30, 31, 34, 0.8);
            display: flex;
            gap: 10px;
            border-top: 1px solid rgba(255,255,255,0.05);
            flex-shrink: 0;
        }
        .aq-btn {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 6px;
            color: #fff;
            font-weight: 600;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s;
            position: relative;
            overflow: hidden;
        }
        .aq-btn:active { transform: scale(0.97); }
        .aq-btn:hover { opacity: 0.9; }
        .aq-btn-start { 
            background: linear-gradient(90deg, ${colors.primary}, #4752C4); 
            box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
        }
        .aq-btn-icon {
            width: 28px; 
            height: 28px; 
            padding: 0; 
            display: flex; 
            align-items: center; 
            justify-content: center; 
            border-radius: 6px;
            cursor: pointer;
            flex: 0 0 auto;
            background: rgba(255,255,255,0.1);
            color: #dbdee1;
            border: none;
            font-family: monospace;
            font-size: 16px;
        }
        .aq-btn-icon:hover { background: rgba(255,255,255,0.2); color: #fff; }
        .aq-btn-close:hover { background: #da373c; }
        .aq-btn-play { 
            background: rgba(88, 101, 242, 0.2); 
            color: #5865F2; 
            width: 32px; 
            height: 32px; 
            border: 1px solid rgba(88, 101, 242, 0.3);
        }
        .aq-btn-play:hover:not(:disabled) { 
            background: #5865F2; 
            color: white; 
            transform: scale(1.05);
        }
        .aq-btn-play:disabled {
            opacity: 0.3;
            cursor: not-allowed;
            filter: grayscale(1);
        }
        
        .aq-resize-handle {
            position: absolute;
            bottom: 0;
            right: 0;
            width: 20px;
            height: 20px;
            cursor: nwse-resize;
            background: radial-gradient(circle at bottom right, rgba(255,255,255,0.2) 0%, transparent 50%);
            border-bottom-right-radius: 12px;
            z-index: 10;
        }
    `;

    // Inject Styles
    const styleEl = document.createElement('style');
    styleEl.textContent = styles;
    document.head.appendChild(styleEl);

    // Create Main Container
    const container = document.createElement('div');
    container.id = 'autoquest-overlay';
    container.innerHTML = `
        <div id="autoquest-header">
            <span style="display:flex; align-items:center; gap:10px;">
                <img src="https://cdn.discordapp.com/embed/avatars/0.png" style="width:24px; height:24px; border-radius:50%;" id="aq-header-icon">
                AutoQuest
            </span>
            <div class="header-controls">
                <button class="aq-btn-icon" id="aq-refresh-btn" title="Refresh Quests">↻</button>
                <button class="aq-btn-icon" id="aq-min-btn" title="Minimize">_</button>
                <button class="aq-btn-icon aq-btn-close" title="Close">×</button>
            </div>
        </div>
        <div id="autoquest-content">
            <div style="text-align: center; color: #949ba4; padding: 40px 20px;">
                <div style="margin-bottom:10px; font-size:24px;">🔍</div>
                Scanning quests...
            </div>
        </div>
        <div id="autoquest-footer">
            <button id="aq-start-btn" class="aq-btn aq-btn-start">Start All Quests</button>
        </div>
        <div class="aq-resize-handle"></div>
    `;
    document.body.appendChild(container);

    // --- State & Interaction Logic ---
    const header = document.getElementById('autoquest-header');
    const resizeHandle = container.querySelector('.aq-resize-handle');
    const minBtn = document.getElementById('aq-min-btn');
    const refreshBtn = document.getElementById('aq-refresh-btn');
    
    let isDragging = false, isResizing = false, isMinimized = false;
    let dragOffset = { x: 0, y: 0 };
    let resizeStart = { w: 0, h: 0, x: 0, y: 0 };
    let rafId = null;
    let savedSize = { w: 380, h: 500 }; 
    let hasMoved = false; 

    const toggleMinimize = () => {
        isMinimized = !isMinimized;
        container.classList.toggle('minimized', isMinimized);
        
        if (isMinimized) {
            savedSize = { w: container.offsetWidth, h: container.offsetHeight };
        } else {
            container.style.width = `${savedSize.w}px`;
            container.style.height = `${savedSize.h}px`;
        }
    };

    minBtn.onclick = (e) => { e.stopPropagation(); toggleMinimize(); };
    
    container.onmousedown = (e) => {
        if (!isMinimized) return; 
        e.preventDefault(); 
        isDragging = true;
        hasMoved = false;
        
        const rect = container.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;
        container.style.cursor = 'grabbing';
    };

    container.onclick = (e) => {
        if(isMinimized && e.target === container && !hasMoved) toggleMinimize();
    };

    header.onmousedown = (e) => {
        if(e.target.closest('button')) return;
        isDragging = true;
        hasMoved = false;
        const rect = container.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;
        header.style.cursor = 'grabbing';
    };

    resizeHandle.onmousedown = (e) => {
        e.stopPropagation();
        isResizing = true;
        const rect = container.getBoundingClientRect();
        resizeStart = { w: rect.width, h: rect.height, x: e.clientX, y: e.clientY };
    };

    document.onmousemove = (e) => {
        if (!isDragging && !isResizing) return;
        if (isDragging) hasMoved = true;
        if (rafId) cancelAnimationFrame(rafId);
        rafId = requestAnimationFrame(() => {
            if (isDragging) {
                const newLeft = e.clientX - dragOffset.x;
                const newTop = e.clientY - dragOffset.y;
                container.style.left = `${newLeft}px`;
                container.style.top = `${newTop}px`;
            } 
            else if (isResizing && !isMinimized) {
                const deltaX = e.clientX - resizeStart.x;
                const deltaY = e.clientY - resizeStart.y;
                container.style.width = `${Math.max(300, resizeStart.w + deltaX)}px`;
                container.style.height = `${Math.max(200, resizeStart.h + deltaY)}px`;
            }
        });
    };

    document.onmouseup = () => {
        isDragging = false;
        isResizing = false;
        header.style.cursor = 'default';
        container.style.cursor = isMinimized ? 'pointer' : 'default';
        if (rafId) cancelAnimationFrame(rafId);
    };

    container.querySelector('.aq-btn-close').onclick = (e) => {
        e.stopPropagation();
        container.remove();
        styleEl.remove();
        if(autoRefreshInterval) clearInterval(autoRefreshInterval);
    };
    
    // --- UI Helpers ---
    const contentArea = document.getElementById('autoquest-content');
    const timers = new Map();
    let autoRefreshInterval = null;

    const formatTime = (seconds) => {
        if (seconds <= 0) return "00:00";
        const m = Math.floor(seconds / 60);
        const s = Math.floor(seconds % 60);
        return `${m}:${s.toString().padStart(2, '0')}`;
    };

    const updateUI = (questId, percent, status, remainingSecs = null, isDone = false) => {
        const card = document.getElementById(`quest-card-${questId}`);
        if (!card) return;
        const bar = card.querySelector('.progress-fill');
        const statusEl = card.querySelector('.status-detail');
        const timerEl = card.querySelector('.quest-timer');
        const playBtn = card.querySelector('.aq-btn-play');

        if(bar) bar.style.width = `${Math.min(100, percent)}%`;
        if(statusEl) statusEl.innerText = status;
        if (remainingSecs !== null && timerEl) timerEl.innerText = formatTime(remainingSecs);
        
        // If status implies running/queued, disable button
        if(playBtn && !isDone && (status.includes("Running") || status.includes("Queued") || status.includes("Starting") || status.includes("Active") || status.includes("Watching"))) {
            playBtn.disabled = true;
            playBtn.innerHTML = `
                <div style="width:12px;height:12px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:aq-spin 1s linear infinite;"></div>
                <style>@keyframes aq-spin{to{transform:rotate(360deg)}}</style>
            `;
        }

        if (isDone) {
            if(bar) { bar.style.background = colors.success; bar.style.boxShadow = "none"; }
            if(statusEl) { statusEl.style.color = colors.success; statusEl.innerText = "Completed!"; }
            if(timerEl) timerEl.innerText = "Done";
            if(playBtn) { playBtn.innerText = "✓"; playBtn.disabled = true; playBtn.style.color = colors.success; playBtn.style.borderColor = colors.success; }
            if (Math.random() > 0.7) fireConfetti(1000, 30); 
            if (timers.has(questId)) { clearInterval(timers.get(questId)); timers.delete(questId); }
        }
    };

    const startLocalTimer = (questId, initialSeconds) => {
        if (timers.has(questId)) clearInterval(timers.get(questId));
        let remaining = initialSeconds;
        const card = document.getElementById(`quest-card-${questId}`);
        const timerEl = card?.querySelector('.quest-timer');
        if(timerEl) timerEl.innerText = formatTime(remaining);
        const interval = setInterval(() => {
            remaining--;
            if (remaining < 0) remaining = 0;
            if(timerEl) timerEl.innerText = formatTime(remaining);
            if(!document.getElementById(`quest-card-${questId}`)) clearInterval(interval);
        }, 1000);
        timers.set(questId, interval);
    };

    // --- Task Queue Scheduler ---
    const Scheduler = {
        video: { active: 0, max: 2, queue: [] },
        game: { active: 0, max: 1, queue: [] },
        stream: { active: 0, max: 1, queue: [] },
        activity: { active: 0, max: 1, queue: [] },

        enqueue: (task) => {
            let type = 'video';
            if(task.type.includes('PLAY_ON_DESKTOP')) type = 'game';
            else if(task.type.includes('STREAM')) type = 'stream';
            else if(task.type.includes('ACTIVITY')) type = 'activity';

            Scheduler[type].queue.push(task);
            updateUI(task.q.id, (task.done/task.needed)*100, "Queued...");
            Scheduler.process(type);
        },

        process: async (type) => {
            const pool = Scheduler[type];
            if (pool.active >= pool.max || pool.queue.length === 0) return;

            pool.active++;
            const task = pool.queue.shift();
            
            try {
                // Execute based on type
                if (type === 'video') {
                    await handleVideoTask(task.q, task.type, task.needed, task.done);
                } else if (type === 'activity') {
                    await handleActivityTask(task.q, task.type, task.needed, task.done);
                } else if (type === 'game') {
                    await handleDesktopPlayTask(task.q, task.appId, task.appName, task.needed, task.pid);
                } else if (type === 'stream') {
                    await handleDesktopStreamTask(task.q, task.appId, task.appName, task.needed, task.pid);
                }
            } catch (e) {
                console.error(e);
            } finally {
                pool.active--;
                // Add delay before next to avoid rate limits
                setTimeout(() => Scheduler.process(type), 1500); 
            }
        }
    };

    const createQuestCard = (q, type) => {
        const div = document.createElement('div');
        div.className = 'quest-card';
        div.id = `quest-card-${q.id}`;
        let typeClass = 'badge-game';
        if(type.includes("VIDEO")) typeClass = 'badge-video';
        if(type.includes("STREAM")) typeClass = 'badge-stream';
        if(type.includes("ACTIVITY")) typeClass = 'badge-activity';
        
        div.innerHTML = `
            <div class="quest-info">
                <div class="quest-meta">
                    <span class="quest-type ${typeClass}">${type.replace(/_/g, ' ')}</span>
                    <span class="quest-timer">--:--</span>
                </div>
                <div class="quest-title">${q.config.messages.questName}</div>
                <div class="progress-track">
                    <div class="progress-fill" style="width: 0%"></div>
                </div>
                <div class="status-text">
                    <span class="status-detail">Ready to start</span>
                </div>
            </div>
            <div class="quest-actions">
                <button class="aq-btn-icon aq-btn-play" title="Start This Quest">▶</button>
            </div>
        `;
        
        // Attach individual start handler
        div.querySelector('.aq-btn-play').onclick = function() {
            this.disabled = true;
            const pid = Math.floor(Math.random() * 30000) + 1000;
            const taskConfig = q.config.taskConfig ?? q.config.taskConfigV2;
            const secondsNeeded = taskConfig.tasks[type].target;
            const secondsDone = q.userStatus?.progress?.[type]?.value ?? 0;
            
            Scheduler.enqueue({
                q: q,
                type: type,
                needed: secondsNeeded,
                done: secondsDone,
                appId: q.config.application.id,
                appName: q.config.application.name,
                pid: pid
            });
        };

        return div;
    };

    // --- 4. Task Logic (Smart Throttle) ---

    // Retry Helper
    const safePost = async (url, body) => {
        try {
            return await api.post({ url, body });
        } catch (e) {
            // Check for 429
            if (e.status === 429 || (e.body && e.body.retry_after)) {
                const retryAfter = (e.body?.retry_after || 5) * 1000;
                console.log(`[Rate Limit] Waiting ${retryAfter}ms...`);
                await new Promise(r => setTimeout(r, retryAfter + 1000));
                return await api.post({ url, body });
            }
            throw e;
        }
    };

    const handleVideoTask = async (quest, taskName, secondsNeeded, secondsDone) => {
        let remaining = secondsNeeded - secondsDone;
        updateUI(quest.id, (secondsDone/secondsNeeded)*100, "Watching...", remaining);
        startLocalTimer(quest.id, remaining);
        
        const maxFuture = 10, speed = 7, interval = 2; // Slower interval
        const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
        
        await new Promise(r => setTimeout(r, Math.random() * 2000));

        while (true) {
            const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
            const diff = maxAllowed - secondsDone;
            const timestamp = secondsDone + speed;

            if (diff >= speed) {
                try {
                    const res = await safePost(`/quests/${quest.id}/video-progress`, { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) });
                    secondsDone = Math.min(secondsNeeded, timestamp);
                    updateUI(quest.id, (secondsDone/secondsNeeded)*100, "Watching...");
                    if (res.body.completed_at != null || secondsDone >= secondsNeeded) break;
                } catch(e) {
                    updateUI(quest.id, (secondsDone/secondsNeeded)*100, "Retrying...");
                }
            }
            await new Promise(r => setTimeout(r, interval * 1000));
        }

        await safePost(`/quests/${quest.id}/video-progress`, { timestamp: secondsNeeded });
        updateUI(quest.id, 100, "Completed!", 0, true);
    };

    const handleActivityTask = async (quest, taskName, secondsNeeded, secondsDone) => {
        const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id 
            ?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0)?.VOCAL[0].channel.id;
        if (!channelId) {
            updateUI(quest.id, 0, "Error: No Voice Channel found");
            return;
        }
        let remaining = secondsNeeded - secondsDone;
        const streamKey = `call:${channelId}:1`;
        updateUI(quest.id, 0, "Active...", remaining);
        startLocalTimer(quest.id, remaining);
        while (true) {
            try {
                const res = await safePost(`/quests/${quest.id}/heartbeat`, { stream_key: streamKey, terminal: false });
                const progress = res.body.progress.PLAY_ACTIVITY.value;
                updateUI(quest.id, (progress/secondsNeeded)*100, "In Activity...");
                if (progress >= secondsNeeded) {
                    await safePost(`/quests/${quest.id}/heartbeat`, { stream_key: streamKey, terminal: true });
                    break;
                }
            } catch(e) {
                updateUI(quest.id, 0, "Retrying...");
            }
            await new Promise(r => setTimeout(r, 20 * 1000));
        }
        updateUI(quest.id, 100, "Completed!", 0, true);
    };

    const handleDesktopPlayTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
        return new Promise((resolve) => {
            if (!isApp) { updateUI(quest.id, 0, "Error: Desktop App Required"); return resolve(); }
            updateUI(quest.id, 0, "Starting Game...", secondsNeeded);
            api.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
                const appData = res.body[0];
                const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", "");
                const fakeGame = { cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, exeName, exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, hidden: false, isLauncher: false, id: applicationId, name: appData.name, pid: pid, pidPath: [pid], processName: appData.name, start: Date.now() };
                const realGetRunningGames = RunningGameStore.getRunningGames;
                const realGetGameForPID = RunningGameStore.getGameForPID;
                const realGames = RunningGameStore.getRunningGames();
                RunningGameStore.getRunningGames = () => [fakeGame];
                RunningGameStore.getGameForPID = (pid) => pid === fakeGame.pid ? fakeGame : null;
                FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: [fakeGame] });
                const fn = (data) => {
                    let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value);
                    const rem = Math.max(0, secondsNeeded - progress);
                    if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);
                    updateUI(quest.id, (progress/secondsNeeded)*100, "Playing Game...", rem);
                    if (progress >= secondsNeeded) {
                        RunningGameStore.getRunningGames = realGetRunningGames;
                        RunningGameStore.getGameForPID = realGetGameForPID;
                        FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] });
                        FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
                        updateUI(quest.id, 100, "Completed!", 0, true);
                        resolve();
                    }
                };
                FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
            });
        });
    };

    const handleDesktopStreamTask = (quest, applicationId, applicationName, secondsNeeded, pid) => {
        return new Promise((resolve) => {
            if (!isApp) { updateUI(quest.id, 0, "Error: Desktop App Required"); return resolve(); }
            updateUI(quest.id, 0, "Starting Stream...", secondsNeeded);
            const realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata;
            ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({ id: applicationId, pid, sourceName: null });
            const fn = (data) => {
                let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value);
                const rem = Math.max(0, secondsNeeded - progress);
                if (!timers.has(quest.id)) startLocalTimer(quest.id, rem);
                updateUI(quest.id, (progress/secondsNeeded)*100, "Streaming...", rem);
                if (progress >= secondsNeeded) {
                    ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc;
                    FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
                    updateUI(quest.id, 100, "Completed!", 0, true);
                    resolve();
                }
            };
            FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn);
        });
    };

    // --- 5. Initialization ---

    const scanQuests = () => {
        const rawQuests = QuestsStore.quests;
        const questsList = (rawQuests instanceof Map) ? [...rawQuests.values()] : Object.values(rawQuests);
        contentArea.innerHTML = '';
        let count = 0;
        
        // Reset schedulers if needed or keep queues? 
        // For simplicity we clear UI and let users re-queue if they refresh manually.
        // Active tasks in background continue running (Scheduler doesn't stop on UI refresh).

        for (const q of questsList) {
            const isEnrolled = !!q.userStatus?.enrolledAt;
            const isCompleted = !!q.userStatus?.completedAt;
            const isExpired = new Date(q.config.expiresAt).getTime() <= Date.now();
            if (q.id === "1412491570820812933") continue; 
            if (isCompleted || isExpired || !isEnrolled) continue;
            const taskConfig = q.config.taskConfig ?? q.config.taskConfigV2;
            const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"].find(x => taskConfig.tasks[x] != null);
            if (!taskName) continue;
            const secondsNeeded = taskConfig.tasks[taskName].target;
            const secondsDone = q.userStatus?.progress?.[taskName]?.value ?? 0;
            if (secondsDone >= secondsNeeded) continue;
            
            contentArea.appendChild(createQuestCard(q, taskName));
            
            const rem = Math.max(0, secondsNeeded - secondsDone);
            const tEl = document.getElementById(`quest-card-${q.id}`).querySelector('.quest-timer');
            if(tEl) tEl.innerText = formatTime(rem);
            count++;
        }
        if(count === 0) { contentArea.innerHTML = '<div style="text-align:center; padding: 40px; color: #949ba4;">No active quests found.<br>Make sure you accepted them!</div>'; return; }
    };

    const startAllQuests = async (btn) => {
        btn.innerText = 'Tasks Queued...';
        btn.disabled = true;
        btn.style.opacity = '0.5';
        
        // Trigger clicks on all visible play buttons
        const playBtns = document.querySelectorAll('.aq-btn-play');
        playBtns.forEach(b => {
            if(!b.disabled) b.click();
        });
    };

    const init = () => {
        scanQuests();
        const startBtn = document.getElementById('aq-start-btn');
        if(startBtn) startBtn.onclick = () => startAllQuests(startBtn);
        if(refreshBtn) {
            refreshBtn.onclick = () => {
                refreshBtn.style.transform = 'rotate(360deg)';
                refreshBtn.style.transition = 'transform 0.5s';
                scanQuests();
                setTimeout(() => { refreshBtn.style.transform = 'none'; }, 500);
            };
        }
        autoRefreshInterval = setInterval(() => {}, 30000);
    };

    init();
    console.log("AutoQuest UI Loaded");
}
@svenbledt
Copy link

Thanks for doing that! Improvements are welcome! i rly like what u did with the project!

@TheDumbWolfy
Copy link

Thanks for making this! Works perfectly!

@BeanoWellesley
Copy link

image_2025-12-21_025513973

It seems that Discord went to work and patched it, I tried running it 3 times with 20 mins of runtime each for tests.

They probably saw your post and smited it down before anyone can bypass

@BeanoWellesley
Copy link

image_2025-12-21_025513973 It seems that Discord went to work and patched it, I tried running it 3 times with 20 mins of runtime each for tests.

They probably saw your post and smited it down before anyone can bypass

nvm, they didnt patch it..

my 2gb laptop just having an aneurysm my bad twin

@qaz1qazlol2
Copy link

qaz1qazlol2 commented Dec 20, 2025

There is a small problem that the refresh button doesn't take place immediately
I have to wait for some time to see the time change
Is it able to make the button take place immediately?

@Rakise
Copy link
Author

Rakise commented Dec 20, 2025

There is a small problem that the refresh button doesn't take place immediately I have to wait for some time to see the time change Is it able to make the button take place immediately?

refresh button is meant to refresh quest list without re-running the code. will add a new functionality to start each one individually

@qaz1qazlol2
Copy link

There is a small problem that the refresh button doesn't take place immediately I have to wait for some time to see the time change Is it able to make the button take place immediately?

refresh button is meant to refresh quest list without re-running the code. will add a new functionality to start each one individually

Great,looking forward to the next update

@Rakise
Copy link
Author

Rakise commented Dec 20, 2025

There is a small problem that the refresh button doesn't take place immediately I have to wait for some time to see the time change Is it able to make the button take place immediately?

refresh button is meant to refresh quest list without re-running the code. will add a new functionality to start each one individually

Great,looking forward to the next update

give it a try! ^-^

@qaz1qazlol2
Copy link

qaz1qazlol2 commented Dec 20, 2025

There is a small problem that the refresh button doesn't take place immediately I have to wait for some time to see the time change Is it able to make the button take place immediately?

refresh button is meant to refresh quest list without re-running the code. will add a new functionality to start each one individually

Great,looking forward to the next update

give it a try! ^-^

wow,update so fast
though i have no more task left to try
maybe next time
by the way,is it able to edit the code to i18n the gui and edit the “Tasks Running” button cursor to 🚫 one?

@Rakise
Copy link
Author

Rakise commented Dec 20, 2025

wow,update so fast though i have no more task left to try maybe next time by the way,is it able to edit the code to i18n the gui and edit the “Tasks Running” button cursor to 🚫 one?

i can try later on yeah

@qaz1qazlol2
Copy link

qaz1qazlol2 commented Dec 20, 2025

i can try later on yeah

Cool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment