Duolingo-SM & Farming Tool

Fastést farming tool for Duolingo

// ==UserScript==
// @name         Duolingo-SM & Farming Tool
// @namespace    http://tampermonkey.net/
// @version      9.9.9
// @description  Fastést farming tool for Duolingo
// @author       DUOS
// @match        https://www.duolingo.com/*
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(() => {
    'use strict';

    const CONFIG = {
        GEM_DELAY: 250,
        XP_DELAY: 2000,
        GEM_BATCH_SIZE: 1,
        XP_AMOUNT: 499
    };

    let jwt, sub, userInfo, headers;
    let activeTask = null;
    let isMinimized = false;
    let stats = { gems: 0, xp: 0, streak: 0 };

    const utils = {
        getJWT: () => {
            const match = document.cookie.match(/jwt_token=([^;]+)/);
            return match ? match[1] : null;
        },

        decodeJWT: (token) => {
            try {
                const payload = token.split('.')[1];
                const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
                return JSON.parse(decodeURIComponent(escape(decoded)));
            } catch (e) {
                return null;
            }
        },

        formatHeaders: (jwt) => ({
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${jwt}`,
            'User-Agent': navigator.userAgent
        }),

        delay: ms => new Promise(r => setTimeout(r, ms)),

        request: async (url, options = {}) => {
            const response = await fetch(url, {
                ...options,
                headers: { ...headers, ...options.headers }
            });
            return response;
        }
    };

    const api = {
        getUserInfo: async () => {
            const url = `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,gems,streakData`;
            const res = await utils.request(url);
            return res.json();
        },

        farmGems: async () => {
            const rewardId = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
            const url = `https://www.duolingo.com/2017-06-30/users/${sub}/rewards/${rewardId}`;
            const data = {
                consumed: true,
                learningLanguage: userInfo.learningLanguage,
                fromLanguage: userInfo.fromLanguage
            };

            return utils.request(url, {
                method: 'PATCH',
                body: JSON.stringify(data)
            });
        },

        farmXP: async () => {
            const url = `https://stories.duolingo.com/api2/stories/fr-en-le-passeport/complete`;
            const data = {
                awardXp: true,
                completedBonusChallenge: true,
                fromLanguage: "en",
                hasXpBoost: false,
                illustrationFormat: "svg",
                isFeaturedStoryInPracticeHub: true,
                isLegendaryMode: true,
                isV2Redo: false,
                isV2Story: false,
                learningLanguage: "fr",
                masterVersion: true,
                maxScore: 0,
                score: 0,
                happyHourBonusXp: 469,
                startTime: Math.floor(Date.now() / 1000),
                endTime: Math.floor(Date.now() / 1000),
            };


            return utils.request(url, {
                method: 'POST',
                body: JSON.stringify(data)
            });
        },

        farmSessionOnce: async (startTime, endTime) => {
            try {
                const challengeTypes = ["assist", "select", "translate", "match", "listen"];

                const sessionPayload = {
                    challengeTypes: challengeTypes,
                    fromLanguage: userInfo.fromLanguage,
                    isFinalLevel: false,
                    isV2: true,
                    juicy: true,
                    learningLanguage: userInfo.learningLanguage,
                    smartTipsVersion: 2,
                    type: "GLOBAL_PRACTICE",
                };

                const sessionRes = await utils.request("https://www.duolingo.com/2017-06-30/sessions", {
                    method: 'POST',
                    body: JSON.stringify(sessionPayload)
                });

                if (!sessionRes?.ok) return null;
                const sessionData = await sessionRes.json();

                if (!sessionData?.id) return null;

                const updatePayload = {
                    ...sessionData,
                    heartsLeft: 0,
                    startTime: startTime,
                    enableBonusPoints: false,
                    endTime: endTime,
                    failed: false,
                    maxInLessonStreak: Math.floor(Math.random() * 10 + 5),
                    shouldLearnThings: true,
                };

                const updateRes = await utils.request(`https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, {
                    method: 'PUT',
                    body: JSON.stringify(updatePayload)
                });

                return updateRes?.ok ? await updateRes.json() : null;
            } catch (error) {
                console.error('Session farm error:', error);
                return null;
            }
        }
    };

    const farming = {
        async gems() {
            while (activeTask === 'gems') {
                const promises = Array(CONFIG.GEM_BATCH_SIZE).fill().map(() => api.farmGems());
                const results = await Promise.allSettled(promises);
                const successful = results.filter(r => r.status === 'fulfilled' && r.value?.ok).length;

                if (successful > 0) {
                    const earned = successful * 30;
                    userInfo.gems += earned;
                    stats.gems += earned;
                    ui.updateStats();
                }

                await utils.delay(CONFIG.GEM_DELAY);
            }
        },

        async xp() {
            while (activeTask === 'xp') {
                const res = await api.farmXP();
                if (res?.ok) {
                    userInfo.totalXp += CONFIG.XP_AMOUNT;
                    stats.xp += CONFIG.XP_AMOUNT;
                    ui.updateStats();
                }
                await utils.delay(CONFIG.XP_DELAY);
            }
        },

        async streak() {
            const hasStreak = userInfo.streakData?.currentStreak;
            const startDate = hasStreak ? userInfo.streakData.currentStreak.startDate : new Date();

            let currentTimestamp = Math.floor(new Date(startDate).getTime() / 1000) - 86400;
            const baseDelay = 40;

            while (activeTask === 'streak') {
                try {
                    const sessionRes = await api.farmSessionOnce(currentTimestamp, currentTimestamp + 300);
                    if (sessionRes) {
                        currentTimestamp -= 86400;
                        userInfo.streak += 1;
                        stats.streak += 1;
                        ui.updateStats();
                        await utils.delay(baseDelay);
                    } else {
                        await utils.delay(baseDelay * 2);
                    }
                } catch (error) {
                    console.error('Streak farming error:', error);
                    await utils.delay(baseDelay * 3);
                }
            }
        },

        stop() {
            activeTask = null;
            ui.updateFarmingButtons();
        }
    };

    const ui = {
        create() {
            const container = document.createElement('div');
            container.id = 'duosm-tool';
            container.innerHTML = `
                <div class="ds-header">
                    <span class="ds-logo">🦉 DuoSM</span>
                    <div class="ds-controls">
                        <button class="ds-btn-minimize">—</button>
                        <button class="ds-btn-close">×</button>
                    </div>
                </div>
                <div class="ds-content">
                    <div class="ds-stats">
                        <div class="ds-stat" data-type="streak">
                            <span class="ds-label">Streak</span>
                            <span class="ds-value" id="streak-val">0</span>
                        </div>
                        <div class="ds-stat" data-type="gems">
                            <span class="ds-label">Gems</span>
                            <span class="ds-value" id="gems-val">0</span>
                        </div>
                        <div class="ds-stat" data-type="xp">
                            <span class="ds-label">XP</span>
                            <span class="ds-value" id="xp-val">0</span>
                        </div>
                    </div>
                    <div class="ds-footer">Made by DUOS</div>
                </div>
                <div class="ds-minimized" style="display: none;">
                    <span>🦉</span>
                </div>
            `;

            const styles = `
                #duosm-tool {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    width: 220px;
                    background: linear-gradient(145deg, #1a1d29, #161926);
                    border: 1px solid #2d3748;
                    border-radius: 16px;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                    box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);
                    backdrop-filter: blur(16px);
                    z-index: 999999;
                    overflow: hidden;
                    transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
                }

                #duosm-tool.minimized {
                    width: 60px;
                    height: 60px;
                    bottom: 20px;
                    top: auto;
                    border-radius: 50%;
                    background: linear-gradient(145deg, #4299e1, #3182ce);
                    cursor: pointer;
                }

                .ds-header {
                    background: linear-gradient(135deg, #2d3748, #1a202c);
                    padding: 12px 16px;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    border-bottom: 1px solid #2d3748;
                    cursor: move;
                }

                .ds-logo {
                    font-weight: 700;
                    font-size: 14px;
                    color: #e2e8f0;
                    letter-spacing: -0.5px;
                }

                .ds-controls {
                    display: flex;
                    gap: 4px;
                }

                .ds-controls button {
                    width: 20px;
                    height: 20px;
                    border: none;
                    background: rgba(255,255,255,0.1);
                    color: #cbd5e0;
                    border-radius: 6px;
                    cursor: pointer;
                    font-size: 12px;
                    transition: all 0.2s;
                }

                .ds-controls button:hover {
                    background: rgba(239,68,68,0.8);
                    transform: scale(1.1);
                }

                .ds-content {
                    padding: 16px;
                }

                .ds-stats {
                    display: grid;
                    gap: 8px;
                }

                .ds-stat {
                    background: linear-gradient(135deg, rgba(74,85,104,0.3), rgba(45,55,72,0.5));
                    border: 1px solid rgba(74,85,104,0.3);
                    border-radius: 12px;
                    padding: 12px 16px;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    cursor: pointer;
                    transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
                    position: relative;
                    overflow: hidden;
                }

                .ds-stat::before {
                    content: '';
                    position: absolute;
                    top: 0;
                    left: -100%;
                    width: 100%;
                    height: 100%;
                    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
                    transition: left 0.6s;
                }

                .ds-stat:hover::before {
                    left: 100%;
                }

                .ds-stat:hover {
                    transform: translateY(-2px) scale(1.02);
                    border-color: rgba(66,153,225,0.6);
                    box-shadow: 0 8px 25px rgba(0,0,0,0.3);
                }

                .ds-stat.active {
                    background: linear-gradient(135deg, rgba(72,187,120,0.2), rgba(56,178,172,0.2));
                    border-color: #48bb78;
                    animation: pulse 2s infinite;
                }

                @keyframes pulse {
                    0%, 100% { box-shadow: 0 0 0 0 rgba(72,187,120,0.4); }
                    50% { box-shadow: 0 0 0 8px rgba(72,187,120,0); }
                }

                .ds-label {
                    font-size: 11px;
                    color: #a0aec0;
                    font-weight: 500;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                .ds-value {
                    font-size: 18px;
                    font-weight: 700;
                    color: #e2e8f0;
                    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
                }

                .ds-footer {
                    text-align: center;
                    margin-top: 12px;
                    font-size: 10px;
                    color: #718096;
                    font-weight: 500;
                    opacity: 0.7;
                }

                .ds-minimized {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    width: 100%;
                    height: 100%;
                    font-size: 24px;
                    cursor: pointer;
                }

                #duosm-tool:not(.minimized) .ds-minimized {
                    display: none !important;
                }

                #duosm-tool.minimized .ds-header,
                #duosm-tool.minimized .ds-content {
                    display: none;
                }

                .ds-stat[data-type="streak"]:hover { border-color: rgba(245,101,101,0.6); }
                .ds-stat[data-type="gems"]:hover { border-color: rgba(56,178,172,0.6); }
                .ds-stat[data-type="xp"]:hover { border-color: rgba(237,137,54,0.6); }

                .ds-stat[data-type="streak"].active {
                    background: linear-gradient(135deg, rgba(245,101,101,0.2), rgba(229,62,62,0.2));
                    border-color: #f56565;
                }
                .ds-stat[data-type="gems"].active {
                    background: linear-gradient(135deg, rgba(56,178,172,0.2), rgba(49,151,149,0.2));
                    border-color: #38b2ac;
                }
                .ds-stat[data-type="xp"].active {
                    background: linear-gradient(135deg, rgba(237,137,54,0.2), rgba(221,107,32,0.2));
                    border-color: #ed8936;
                }
            `;

            document.head.insertAdjacentHTML('beforeend', `<style>${styles}</style>`);
            document.body.appendChild(container);

            this.setupEvents(container);
            this.makeDraggable(container);
        },

        setupEvents(container) {
            container.querySelector('.ds-btn-minimize').onclick = () => this.toggleMinimize();
            container.querySelector('.ds-btn-close').onclick = () => container.remove();
            container.querySelector('.ds-minimized').onclick = () => this.toggleMinimize();

            container.querySelectorAll('.ds-stat').forEach(stat => {
                stat.onclick = () => {
                    const type = stat.dataset.type;
                    if (activeTask === type) {
                        farming.stop();
                    } else {
                        farming.stop();
                        activeTask = type;
                        farming[type]();
                        this.updateFarmingButtons();
                    }
                };
            });
        },

        makeDraggable(element) {
            let pos = { x: 0, y: 0, startX: 0, startY: 0 };
            const header = element.querySelector('.ds-header');

            const dragStart = (e) => {
                pos.startX = e.clientX;
                pos.startY = e.clientY;
                document.onmousemove = drag;
                document.onmouseup = dragEnd;
            };

            const drag = (e) => {
                pos.x = pos.startX - e.clientX;
                pos.y = pos.startY - e.clientY;
                pos.startX = e.clientX;
                pos.startY = e.clientY;
                element.style.top = (element.offsetTop - pos.y) + 'px';
                element.style.left = (element.offsetLeft - pos.x) + 'px';
                element.style.right = 'auto';
                element.style.bottom = 'auto';
            };

            const dragEnd = () => {
                document.onmouseup = null;
                document.onmousemove = null;
            };

            header.onmousedown = dragStart;
        },

        toggleMinimize() {
            isMinimized = !isMinimized;
            const tool = document.getElementById('duosm-tool');
            tool.classList.toggle('minimized', isMinimized);
        },

        updateStats() {
            if (!userInfo) return;
            document.getElementById('streak-val').textContent = userInfo.streak || 0;
            document.getElementById('gems-val').textContent = userInfo.gems || 0;
            document.getElementById('xp-val').textContent = (userInfo.totalXp || 0).toLocaleString();
        },

        updateFarmingButtons() {
            document.querySelectorAll('.ds-stat').forEach(stat => {
                stat.classList.toggle('active', activeTask === stat.dataset.type);
            });
        }
    };

    const init = async () => {
        if (!location.hostname.includes('duolingo.com')) return;

        jwt = utils.getJWT();
        if (!jwt) return;

        const decoded = utils.decodeJWT(jwt);
        if (!decoded) return;

        sub = decoded.sub;
        headers = utils.formatHeaders(jwt);

        try {
            userInfo = await api.getUserInfo();
            ui.create();
            ui.updateStats();
        } catch (error) {
            console.error('DuoSM init failed:', error);
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 1000);
    }
})();