YM Duo_KeepStreak

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
    };
})();