// ==UserScript==
// @name DUO_KEEPSTREAK
// @namespace ´꒳`ⓎⒶⓂⒾⓈⒸⓇⒾⓅⓉ×͜×
// @version v1.0.3
// @description Automatically maintains the daily streak on Duolingo (ADD WORD"PROCESSING" 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 (button) => {
button.innerText = "Processing...";
button.disabled = true;
button.style.opacity = "0.7";
const token = getToken();
if (!token) {
alert("❌ You are not logged into Duolingo!");
} else {
const userId = parseJwt(token)?.sub;
if (!userId) {
alert("❌ Error retrieving user ID.");
} else {
const headers = getHeaders(token);
const userData = await fetchUserData(userId, headers);
if (!userData) {
alert("⚠️ Error fetching user data, try again!");
} else if (hasStreakToday(userData)) {
alert("✅ You have already maintained your streak today!");
} else {
const session = await startSession(userData.fromLanguage, userData.learningLanguage, headers);
if (!session) {
alert("⚠️ Error starting session, try again!");
} else {
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!");
}
}
}
}
}
button.innerText = "🔥 Get Streak 🔥";
button.disabled = false;
button.style.opacity = "1";
};
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(button);
document.body.appendChild(button);
};
window.onload = () => {
addConfettiStyle();
setTimeout(addButton, 2000);
};