// ==UserScript==
// @name Duolingo-SM & Farming Tool
// @namespace http://tampermonkey.net/
// @version 9.9.9
// @description Fastést farming tool for Duolingo
// @author DUOS
// @match https://www.duolingo.com/*
// @grant none
// @license MIT
// @run-at document-end
// ==/UserScript==
(() => {
'use strict';
const CONFIG = {
GEM_DELAY: 250,
XP_DELAY: 2000,
GEM_BATCH_SIZE: 1,
XP_AMOUNT: 499
};
let jwt, sub, userInfo, headers;
let activeTask = null;
let isMinimized = false;
let stats = { gems: 0, xp: 0, streak: 0 };
const utils = {
getJWT: () => {
const match = document.cookie.match(/jwt_token=([^;]+)/);
return match ? match[1] : null;
},
decodeJWT: (token) => {
try {
const payload = token.split('.')[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decodeURIComponent(escape(decoded)));
} catch (e) {
return null;
}
},
formatHeaders: (jwt) => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`,
'User-Agent': navigator.userAgent
}),
delay: ms => new Promise(r => setTimeout(r, ms)),
request: async (url, options = {}) => {
const response = await fetch(url, {
...options,
headers: { ...headers, ...options.headers }
});
return response;
}
};
const api = {
getUserInfo: async () => {
const url = `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,gems,streakData`;
const res = await utils.request(url);
return res.json();
},
farmGems: async () => {
const rewardId = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
const url = `https://www.duolingo.com/2017-06-30/users/${sub}/rewards/${rewardId}`;
const data = {
consumed: true,
learningLanguage: userInfo.learningLanguage,
fromLanguage: userInfo.fromLanguage
};
return utils.request(url, {
method: 'PATCH',
body: JSON.stringify(data)
});
},
farmXP: async () => {
const url = `https://stories.duolingo.com/api2/stories/fr-en-le-passeport/complete`;
const data = {
awardXp: true,
completedBonusChallenge: true,
fromLanguage: "en",
hasXpBoost: false,
illustrationFormat: "svg",
isFeaturedStoryInPracticeHub: true,
isLegendaryMode: true,
isV2Redo: false,
isV2Story: false,
learningLanguage: "fr",
masterVersion: true,
maxScore: 0,
score: 0,
happyHourBonusXp: 469,
startTime: Math.floor(Date.now() / 1000),
endTime: Math.floor(Date.now() / 1000),
};
return utils.request(url, {
method: 'POST',
body: JSON.stringify(data)
});
},
farmSessionOnce: async (startTime, endTime) => {
try {
const challengeTypes = ["assist", "select", "translate", "match", "listen"];
const sessionPayload = {
challengeTypes: challengeTypes,
fromLanguage: userInfo.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: userInfo.learningLanguage,
smartTipsVersion: 2,
type: "GLOBAL_PRACTICE",
};
const sessionRes = await utils.request("https://www.duolingo.com/2017-06-30/sessions", {
method: 'POST',
body: JSON.stringify(sessionPayload)
});
if (!sessionRes?.ok) return null;
const sessionData = await sessionRes.json();
if (!sessionData?.id) return null;
const updatePayload = {
...sessionData,
heartsLeft: 0,
startTime: startTime,
enableBonusPoints: false,
endTime: endTime,
failed: false,
maxInLessonStreak: Math.floor(Math.random() * 10 + 5),
shouldLearnThings: true,
};
const updateRes = await utils.request(`https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, {
method: 'PUT',
body: JSON.stringify(updatePayload)
});
return updateRes?.ok ? await updateRes.json() : null;
} catch (error) {
console.error('Session farm error:', error);
return null;
}
}
};
const farming = {
async gems() {
while (activeTask === 'gems') {
const promises = Array(CONFIG.GEM_BATCH_SIZE).fill().map(() => api.farmGems());
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled' && r.value?.ok).length;
if (successful > 0) {
const earned = successful * 30;
userInfo.gems += earned;
stats.gems += earned;
ui.updateStats();
}
await utils.delay(CONFIG.GEM_DELAY);
}
},
async xp() {
while (activeTask === 'xp') {
const res = await api.farmXP();
if (res?.ok) {
userInfo.totalXp += CONFIG.XP_AMOUNT;
stats.xp += CONFIG.XP_AMOUNT;
ui.updateStats();
}
await utils.delay(CONFIG.XP_DELAY);
}
},
async streak() {
const hasStreak = userInfo.streakData?.currentStreak;
const startDate = hasStreak ? userInfo.streakData.currentStreak.startDate : new Date();
let currentTimestamp = Math.floor(new Date(startDate).getTime() / 1000) - 86400;
const baseDelay = 40;
while (activeTask === 'streak') {
try {
const sessionRes = await api.farmSessionOnce(currentTimestamp, currentTimestamp + 300);
if (sessionRes) {
currentTimestamp -= 86400;
userInfo.streak += 1;
stats.streak += 1;
ui.updateStats();
await utils.delay(baseDelay);
} else {
await utils.delay(baseDelay * 2);
}
} catch (error) {
console.error('Streak farming error:', error);
await utils.delay(baseDelay * 3);
}
}
},
stop() {
activeTask = null;
ui.updateFarmingButtons();
}
};
const ui = {
create() {
const container = document.createElement('div');
container.id = 'duosm-tool';
container.innerHTML = `
<div class="ds-header">
<span class="ds-logo">🦉 DuoSM</span>
<div class="ds-controls">
<button class="ds-btn-minimize">—</button>
<button class="ds-btn-close">×</button>
</div>
</div>
<div class="ds-content">
<div class="ds-stats">
<div class="ds-stat" data-type="streak">
<span class="ds-label">Streak</span>
<span class="ds-value" id="streak-val">0</span>
</div>
<div class="ds-stat" data-type="gems">
<span class="ds-label">Gems</span>
<span class="ds-value" id="gems-val">0</span>
</div>
<div class="ds-stat" data-type="xp">
<span class="ds-label">XP</span>
<span class="ds-value" id="xp-val">0</span>
</div>
</div>
<div class="ds-footer">Made by DUOS</div>
</div>
<div class="ds-minimized" style="display: none;">
<span>🦉</span>
</div>
`;
const styles = `
#duosm-tool {
position: fixed;
top: 20px;
right: 20px;
width: 220px;
background: linear-gradient(145deg, #1a1d29, #161926);
border: 1px solid #2d3748;
border-radius: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);
backdrop-filter: blur(16px);
z-index: 999999;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#duosm-tool.minimized {
width: 60px;
height: 60px;
bottom: 20px;
top: auto;
border-radius: 50%;
background: linear-gradient(145deg, #4299e1, #3182ce);
cursor: pointer;
}
.ds-header {
background: linear-gradient(135deg, #2d3748, #1a202c);
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #2d3748;
cursor: move;
}
.ds-logo {
font-weight: 700;
font-size: 14px;
color: #e2e8f0;
letter-spacing: -0.5px;
}
.ds-controls {
display: flex;
gap: 4px;
}
.ds-controls button {
width: 20px;
height: 20px;
border: none;
background: rgba(255,255,255,0.1);
color: #cbd5e0;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.ds-controls button:hover {
background: rgba(239,68,68,0.8);
transform: scale(1.1);
}
.ds-content {
padding: 16px;
}
.ds-stats {
display: grid;
gap: 8px;
}
.ds-stat {
background: linear-gradient(135deg, rgba(74,85,104,0.3), rgba(45,55,72,0.5));
border: 1px solid rgba(74,85,104,0.3);
border-radius: 12px;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
position: relative;
overflow: hidden;
}
.ds-stat::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.6s;
}
.ds-stat:hover::before {
left: 100%;
}
.ds-stat:hover {
transform: translateY(-2px) scale(1.02);
border-color: rgba(66,153,225,0.6);
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
}
.ds-stat.active {
background: linear-gradient(135deg, rgba(72,187,120,0.2), rgba(56,178,172,0.2));
border-color: #48bb78;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(72,187,120,0.4); }
50% { box-shadow: 0 0 0 8px rgba(72,187,120,0); }
}
.ds-label {
font-size: 11px;
color: #a0aec0;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ds-value {
font-size: 18px;
font-weight: 700;
color: #e2e8f0;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.ds-footer {
text-align: center;
margin-top: 12px;
font-size: 10px;
color: #718096;
font-weight: 500;
opacity: 0.7;
}
.ds-minimized {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 24px;
cursor: pointer;
}
#duosm-tool:not(.minimized) .ds-minimized {
display: none !important;
}
#duosm-tool.minimized .ds-header,
#duosm-tool.minimized .ds-content {
display: none;
}
.ds-stat[data-type="streak"]:hover { border-color: rgba(245,101,101,0.6); }
.ds-stat[data-type="gems"]:hover { border-color: rgba(56,178,172,0.6); }
.ds-stat[data-type="xp"]:hover { border-color: rgba(237,137,54,0.6); }
.ds-stat[data-type="streak"].active {
background: linear-gradient(135deg, rgba(245,101,101,0.2), rgba(229,62,62,0.2));
border-color: #f56565;
}
.ds-stat[data-type="gems"].active {
background: linear-gradient(135deg, rgba(56,178,172,0.2), rgba(49,151,149,0.2));
border-color: #38b2ac;
}
.ds-stat[data-type="xp"].active {
background: linear-gradient(135deg, rgba(237,137,54,0.2), rgba(221,107,32,0.2));
border-color: #ed8936;
}
`;
document.head.insertAdjacentHTML('beforeend', `<style>${styles}</style>`);
document.body.appendChild(container);
this.setupEvents(container);
this.makeDraggable(container);
},
setupEvents(container) {
container.querySelector('.ds-btn-minimize').onclick = () => this.toggleMinimize();
container.querySelector('.ds-btn-close').onclick = () => container.remove();
container.querySelector('.ds-minimized').onclick = () => this.toggleMinimize();
container.querySelectorAll('.ds-stat').forEach(stat => {
stat.onclick = () => {
const type = stat.dataset.type;
if (activeTask === type) {
farming.stop();
} else {
farming.stop();
activeTask = type;
farming[type]();
this.updateFarmingButtons();
}
};
});
},
makeDraggable(element) {
let pos = { x: 0, y: 0, startX: 0, startY: 0 };
const header = element.querySelector('.ds-header');
const dragStart = (e) => {
pos.startX = e.clientX;
pos.startY = e.clientY;
document.onmousemove = drag;
document.onmouseup = dragEnd;
};
const drag = (e) => {
pos.x = pos.startX - e.clientX;
pos.y = pos.startY - e.clientY;
pos.startX = e.clientX;
pos.startY = e.clientY;
element.style.top = (element.offsetTop - pos.y) + 'px';
element.style.left = (element.offsetLeft - pos.x) + 'px';
element.style.right = 'auto';
element.style.bottom = 'auto';
};
const dragEnd = () => {
document.onmouseup = null;
document.onmousemove = null;
};
header.onmousedown = dragStart;
},
toggleMinimize() {
isMinimized = !isMinimized;
const tool = document.getElementById('duosm-tool');
tool.classList.toggle('minimized', isMinimized);
},
updateStats() {
if (!userInfo) return;
document.getElementById('streak-val').textContent = userInfo.streak || 0;
document.getElementById('gems-val').textContent = userInfo.gems || 0;
document.getElementById('xp-val').textContent = (userInfo.totalXp || 0).toLocaleString();
},
updateFarmingButtons() {
document.querySelectorAll('.ds-stat').forEach(stat => {
stat.classList.toggle('active', activeTask === stat.dataset.type);
});
}
};
const init = async () => {
if (!location.hostname.includes('duolingo.com')) return;
jwt = utils.getJWT();
if (!jwt) return;
const decoded = utils.decodeJWT(jwt);
if (!decoded) return;
sub = decoded.sub;
headers = utils.formatHeaders(jwt);
try {
userInfo = await api.getUserInfo();
ui.create();
ui.updateStats();
} catch (error) {
console.error('DuoSM init failed:', error);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 1000);
}
})();