DUO_KEEPSTREAK

Automatically maintains the daily streak on Duolingo (NEW UPDATE)

目前为 2025-03-24 提交的版本。查看 最新版本

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

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`, { 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;
    }
};

// 🎉 Hiệu ứng giấy màu rơi xuống 🎉
const createConfetti = () => {
    const confettiCount = 100;
    for (let i = 0; i < confettiCount; i++) {
        const confetti = document.createElement("div");
        confetti.classList.add("confetti");
        document.body.appendChild(confetti);

        // Tạo màu sắc ngẫu nhiên
        confetti.style.backgroundColor = `hsl(${Math.random() * 360}, 100%, 50%)`;
        confetti.style.width = `${6 + Math.random() * 6}px`;
        confetti.style.height = confetti.style.width;
        confetti.style.position = "fixed";
        confetti.style.top = "-10px";
        confetti.style.left = `${Math.random() * 100}vw`;
        confetti.style.opacity = "0.8";
        confetti.style.borderRadius = "50%";

        // Random tốc độ rơi
        const duration = 2 + Math.random() * 3;
        confetti.style.animation = `fall ${duration}s ease-out forwards, spin ${duration}s linear infinite`;

        // Xóa sau khi animation kết thúc
        setTimeout(() => {
            confetti.remove();
        }, duration * 1000);
    }
};

// Thêm CSS animation
const addConfettiStyle = () => {
    const style = document.createElement("style");
    style.innerHTML = `
        .confetti {
            position: fixed;
            top: 0;
        }
        @keyframes fall {
            to {
                transform: translateY(100vh);
                opacity: 0;
            }
        }
        @keyframes spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
    `;
    document.head.appendChild(style);
};

const attemptStreak = async () => {
    const token = getToken();
    if (!token) return alert("❌ You are not logged into Duolingo!");

    const userId = parseJwt(token)?.sub;
    if (!userId) return alert("❌ Error retrieving user ID.");

    const headers = getHeaders(token);
    const userData = await fetchUserData(userId, headers);
    if (!userData) return alert("⚠️ Error fetching user data, try again!");

    if (hasStreakToday(userData)) return alert("✅ You have already maintained your streak today!");

    const session = await startSession(userData.fromLanguage, userData.learningLanguage, headers);
    if (!session) return alert("⚠️ Error starting session, try again!");

    const completed = await completeSession(session, headers);
    if (completed) {
        alert("🎉 Streak has been maintained! Reload the page to check.");
        createConfetti();
    } else {
        alert("⚠️ Error maintaining streak, try again!");
    }
};

const addButton = () => {
    if (document.getElementById("get-streak-btn")) return;

    const button = document.createElement("button");
    button.id = "get-streak-btn";
    button.innerText = "🔥 Get Streak 🔥";
    button.style.position = "fixed";
    button.style.bottom = "20px";
    button.style.right = "20px";
    button.style.padding = "14px 24px";
    button.style.backgroundColor = "#58cc02";
    button.style.color = "white";
    button.style.fontSize = "18px";
    button.style.fontWeight = "bold";
    button.style.border = "none";
    button.style.borderRadius = "30px";
    button.style.boxShadow = "0px 6px 12px rgba(0, 0, 0, 0.2)";
    button.style.cursor = "pointer";
    button.style.zIndex = "1000";
    button.style.transition = "all 0.2s ease-in-out";

    button.onmouseover = () => {
        button.style.backgroundColor = "#46a102";
        button.style.transform = "scale(1.1)";
    };

    button.onmouseout = () => {
        button.style.backgroundColor = "#58cc02";
        button.style.transform = "scale(1)";
    };

    button.onclick = attemptStreak;
    document.body.appendChild(button);
};

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址