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