Duo_KeepStreak

Automatically maintains the daily streak on Duolingo.

// ==UserScript==
// @name         Duo_KeepStreak
// @namespace    HACKER_DUOLINGO_666
// @version      1.0.1
// @description  Automatically maintains the daily streak on Duolingo.
// @author       HACKER_DUOLINGO_666
// @match        https://*.duolingo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// ==/UserScript==
const getToken = () => document.cookie.split('; ').find(row => row.startsWith('jwt_token='))?.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) => {
    const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${userId}?fields=fromLanguage,learningLanguage,streakData`, { headers });
    return response.json();
};

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

const startSession = async (fromLang, learningLang, headers) => {
    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) });
    return response.json();
};

const completeSession = async (session, headers) => {
    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) });
    return response.json();
};

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 (hasStreakToday(userData)) return alert("✅ You have already maintained your streak today!");

    try {
        const session = await startSession(userData.fromLanguage, userData.learningLanguage, headers);
        await completeSession(session, headers);
        alert("🎉 Streak has been maintained! Reload the page to check.");
    } catch (error) {
        console.error("Streak maintenance error", error);
        alert("⚠️ Error maintaining streak, try again!");
    }
};
const addButton = () => {
    const existingButton = document.getElementById("get-streak-btn");
    if (existingButton) return; // Tránh tạo nút trùng lặp

    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 = "10px 15px";
    button.style.backgroundColor = "#58cc02";
    button.style.color = "white";
    button.style.fontSize = "16px";
    button.style.border = "none";
    button.style.borderRadius = "5px";
    button.style.cursor = "pointer";
    button.style.zIndex = "1000";

    button.onclick = attemptStreak;

    document.body.appendChild(button);
};

// Đợi trang tải xong rồi thêm nút
window.onload = () => {
    setTimeout(addButton, 2000);
};