YM Duo_KeepStreak

Automatically maintains the daily streak on Duolingo (NEW VERSION V1.0.5)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YM Duo_KeepStreak
// @namespace    ´꒳`ⓎⒶⓂⒾⓈⒸⓇⒾⓅⓉ×͜×
// @version      v1.0.5
// @description  Automatically maintains the daily streak on Duolingo (NEW VERSION V1.0.5)
// @author       ´꒳`ⓎⒶⓂⒾⓈⒸⓇⒾⓅⓉ×͜×
// @match        https://*.duolingo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// ==/UserScript==

(function () {
    'use strict';

    const getToken = () => {
        const tokenRow = document.cookie.split('; ').find(row => row.startsWith('jwt_token='));
        return tokenRow ? tokenRow.split('=')[1] : null;
    };

    const parseJwt = (token) => {
        try {
            return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
        } catch (e) {
            console.error("JWT parsing error", e);
            return null;
        }
    };

    const getHeaders = (token) => ({
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`,
        "User-Agent": navigator.userAgent
    });

    const fetchUserData = async (userId, headers) => {
        try {
            const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${userId}?fields=fromLanguage,learningLanguage,streakData,subscriptions`, { headers });
            if (!response.ok) throw new Error("Failed to fetch user data");
            return response.json();
        } catch (error) {
            console.error("Error fetching user data:", error);
            return null;
        }
    };

    const hasStreakToday = (data) => {
        const today = new Date().toISOString().split('T')[0];
        return data?.streakData?.currentStreak?.endDate === today;
    };

    const startSession = async (fromLang, learningLang, headers) => {
        try {
            const payload = {
                challengeTypes: ["translate", "match", "tapComplete", "reverseAssist", "judge"],
                fromLanguage: fromLang,
                learningLanguage: learningLang,
                type: "GLOBAL_PRACTICE"
            };
            const response = await fetch("https://www.duolingo.com/2017-06-30/sessions", {
                method: 'POST',
                headers,
                body: JSON.stringify(payload)
            });
            if (!response.ok) throw new Error("Failed to start session");
            return response.json();
        } catch (error) {
            console.error("Error starting session:", error);
            return null;
        }
    };

    const completeSession = async (session, headers) => {
        try {
            const payload = { ...session, heartsLeft: 0, failed: false, shouldLearnThings: true };
            const response = await fetch(`https://www.duolingo.com/2017-06-30/sessions/${session.id}`, {
                method: 'PUT',
                headers,
                body: JSON.stringify(payload)
            });
            if (!response.ok) throw new Error("Failed to complete session");
            return response.json();
        } catch (error) {
            console.error("Error completing session:", error);
            return null;
        }
    };

    const isVipUser = (userData) => {
        return userData?.subscriptions?.some(sub => sub.type === "VIP");
    };

    const createConfetti = () => {
        const confettiContainer = document.createElement("div");
        confettiContainer.className = "confetti";
        for (let i = 0; i < 100; i++) {
            const div = document.createElement("div");
            div.style.left = `${Math.random() * 100}%`;
            div.style.backgroundColor = `hsl(${Math.random() * 360}, 100%, 50%)`;
            div.style.animationDelay = `${Math.random() * 3}s`;
            confettiContainer.appendChild(div);
        }
        document.body.appendChild(confettiContainer);
        setTimeout(() => confettiContainer.remove(), 3000);
    };

    const addConfettiStyle = () => {
        const style = document.createElement("style");
        style.innerHTML = `
            @keyframes confetti {
                0% { transform: translateY(0); opacity: 1; }
                100% { transform: translateY(100vh); opacity: 0; }
            }
            .confetti {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                pointer-events: none;
                z-index: 9999;
            }
            .confetti div {
                position: absolute;
                width: 10px;
                height: 10px;
                opacity: 0.8;
                animation: confetti 3s infinite;
            }
        `;
        document.head.appendChild(style);
    };

    const attemptStreak = async (button) => {
        button.innerText = "⏳ Processing...";
        button.disabled = true;

        const token = getToken();
        if (!token) {
            alert("❌ Bạn chưa đăng nhập vào Duolingo!");
        } else {
            const userId = parseJwt(token)?.sub;
            if (!userId) {
                alert("❌ Không thể lấy thông tin người dùng.");
            } else {
                const headers = getHeaders(token);
                const userData = await fetchUserData(userId, headers);

                if (!userData) {
                    alert("⚠️ Không thể tải dữ liệu người dùng.");
                } else if (hasStreakToday(userData)) {
                    alert("✅ Bạn đã duy trì streak hôm nay!");
                } else {
                    if (isVipUser(userData)) {
                        alert("🌟 VIP User! Tận hưởng quyền lợi cao cấp.");
                    }

                    const session = await startSession(userData.fromLanguage, userData.learningLanguage, headers);
                    if (!session) {
                        alert("⚠️ Không thể bắt đầu phiên học.");
                    } else {
                        const completed = await completeSession(session, headers);
                        if (completed) {
                            const xpBonus = isVipUser(userData) ? 20 : 10;
                            alert(`🎉 Đã duy trì streak! Nhận được ${xpBonus} XP.`);
                            createConfetti();
                        } else {
                            alert("⚠️ Không thể hoàn thành phiên học.");
                        }
                    }
                }
            }
        }

        button.innerText = "🔥 Get Streak 🔥";
        button.disabled = false;
    };

    const createControlPanel = async () => {
        if (document.getElementById("duo-panel")) return;

        const panel = document.createElement("div");
        panel.id = "duo-panel";
        panel.style.position = "fixed";
        panel.style.bottom = "20px";
        panel.style.right = "20px";
        panel.style.background = "white";
        panel.style.border = "2px solid #58cc02";
        panel.style.borderRadius = "16px";
        panel.style.padding = "12px";
        panel.style.zIndex = "9999";
        panel.style.boxShadow = "0 4px 10px rgba(0,0,0,0.15)";
        panel.style.transition = "all 0.3s ease-in-out";
        panel.style.minWidth = "180px";

        const btn = document.createElement("button");
        btn.innerText = "🔥 Get Streak 🔥";
        btn.style.width = "100%";
        btn.style.padding = "10px";
        btn.style.marginBottom = "10px";
        btn.style.backgroundColor = "#58cc02";
        btn.style.color = "white";
        btn.style.border = "none";
        btn.style.borderRadius = "10px";
        btn.style.cursor = "pointer";
        btn.onclick = () => attemptStreak(btn);

        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.id = "duo-auto-run";
        checkbox.checked = localStorage.getItem("duo_auto_run") === "true";
        checkbox.onchange = () => {
            localStorage.setItem("duo_auto_run", checkbox.checked);
        };

        const label = document.createElement("label");
        label.htmlFor = "duo-auto-run";
        label.style.fontSize = "14px";
        label.style.color = "#333";
        label.style.display = "flex";
        label.style.alignItems = "center";
        label.innerHTML = `<span style="margin-left: 8px;">Auto Run</span>`;
        label.prepend(checkbox);

        const toggleBtn = document.createElement("button");
        toggleBtn.innerText = "🔽";
        toggleBtn.style.position = "fixed";
        toggleBtn.style.bottom = "20px";
        toggleBtn.style.right = "220px";
        toggleBtn.style.width = "32px";
        toggleBtn.style.height = "32px";
        toggleBtn.style.borderRadius = "50%";
        toggleBtn.style.border = "none";
        toggleBtn.style.background = "#58cc02";
        toggleBtn.style.color = "white";
        toggleBtn.style.cursor = "pointer";
        toggleBtn.style.zIndex = "9999";
        toggleBtn.title = "Hiện/Ẩn Panel";

        let visible = true;
        toggleBtn.onclick = () => {
            visible = !visible;
            panel.style.display = visible ? "block" : "none";
            toggleBtn.innerText = visible ? "🔽" : "🔼";
        };

        panel.appendChild(btn);
        panel.appendChild(label);
        document.body.appendChild(panel);
        document.body.appendChild(toggleBtn);

        // Tự chạy nếu Auto Run bật và chưa có streak
        if (checkbox.checked) {
            const token = getToken();
            if (token) {
                const userId = parseJwt(token)?.sub;
                if (userId) {
                    const headers = getHeaders(token);
                    const userData = await fetchUserData(userId, headers);
                    if (userData && !hasStreakToday(userData)) {
                        setTimeout(() => attemptStreak(btn), 1000);
                    }
                }
            }
        }
    };

    window.onload = () => {
        addConfettiStyle();
        setTimeout(createControlPanel, 2000);
    };
})();