// ==UserScript==
// @name Hacklingo
// @name:vi Hacklingo - Công cụ farm XP
// @name:en Hacklingo - XP Farming Tool
// @name:es Hacklingo - Herramienta de farmeo de XP
// @name:fr Hacklingo - Outil de farm XP
// @name:de Hacklingo - XP Farming Tool
// @name:pt-BR Hacklingo - Ferramenta de farm XP
// @name:ru Hacklingo - Инструмент для фарма XP
// @name:zh-CN Hacklingo - XP刷分工具
// @name:ja Hacklingo - XPファーミングツール
// @name:ko Hacklingo - XP 파밍 도구
// @description Hacklingo — tool for farming XP, streaks, and gems with ultra-fast multi-thread support.
// @description:vi Hacklingo — công cụ farm XP, streaks và gems với tốc độ cao nhờ multi-thread.
// @description:en Hacklingo — tool for farming XP, streaks, and gems with ultra-fast multi-thread support.
// @description:es Hacklingo — herramienta para farmear XP, rachas y gemas con soporte multi-hilo de alta velocidad.
// @description:fr Hacklingo — outil pour farmer XP, séries et gemmes avec un support multi-thread ultra-rapide.
// @description:de Hacklingo — Tool zum Farmen von XP, Serien und Edelsteinen mit ultraschneller Multi-Thread-Unterstützung.
// @description:pt-BR Hacklingo — ferramenta para farmar XP, streaks e gemas com suporte multi-thread de alta velocidade.
// @description:ru Hacklingo — инструмент для фарма XP, серий и самоцветов с ультра-быстрой многопоточностью.
// @description:zh-CN Hacklingo — 支持高速多线程的XP、连胜和宝石刷分工具。
// @description:ja Hacklingo — XP・連続日数・ジェムを超高速マルチスレッドで稼ぐツール。
// @description:ko Hacklingo — 초고속 멀티스레드로 XP, 연속 기록, 보석을 파밍하는 도구.
// @namespace https://twisk.fun
// @version 1.0.1
// @author airpl4ne
// @author Airplane Mode
// @author S
// @match https://*.duolingo.com/*
// @icon https://github.com/pillowslua/crackduo/blob/main/hacklingo.png?raw=true
// @grant none
// @license MIT
// ==/UserScript==
const VERSION = "1.0.0"; // version
const BASE_DELAY = 500; // Anti-rate limit . Please dont change this
const MAX_CONCURRENT_REQUESTS = 50; // Max concurrent requests
const DISCORD_LINK = "https://discord.gg/m3EV55SpYw"; // Discord server link
const REAPPEAR_DELAY = 10000; // delay !!
var jwt, defaultHeaders, userInfo, sub;
let isRunning = false;
let threadCount = 1; // Default thread count
let requestQueue = []; // quueu
let activeRequests = 0; // active rqs
let rateLimitDelay = BASE_DELAY; // delay set
// user agent!
const USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.10 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:136.0) Gecko/20100101 Firefox/136.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Trailer/93.3.8652.5"
];
const initInterface = () => {
const containerHTML = `
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<div id="_overlay"></div>
<div id="_container">
<div id="_header">
<span class="_label">Hacklingo v1.0.1</span>
<button id="_close_btn">✕</button>
</div>
<div id="_body">
<div id="_user_card" class="_card">
<h3>User Info</h3>
<div class="_info_row"><span>Username:</span><span id="_username">duofarmer</span></div>
<div class="_info_row"><span>From:</span><span id="_from">any</span></div>
<div class="_info_row"><span>Learning:</span><span id="_learn">any</span></div>
</div>
<div id="_progress_card" class="_card">
<h3>Progress</h3>
<div class="_info_row"><span>Streak:</span><span id="_streak">0</span></div>
<div class="_info_row"><span>Gem:</span><span id="_gem">0</span></div>
<div class="_info_row"><span>XP:</span><span id="_xp">0</span></div>
</div>
<div id="_controls_card" class="_card">
<h3>Controls</h3>
<select id="_select_option"></select>
<div class="_thread_row">
<label for="_thread_count">Threads (1+, high values may still hit limits):</label>
<input type="number" id="_thread_count" min="1" value="1">
</div>
<div id="_action_row">
<button id="_start_btn">Start Farming</button>
<button id="_stop_btn" hidden>Stop Farming</button>
</div>
</div>
<div id="_notify_card" class="_card">
<p id="_notify">Welcome! Select an option and start farming. Supports English learning only. For best performance, use a blank page. User-agent rotation and adaptive delays enabled to handle rate limits better. Join our Discord for support!</p>
<a id="_blank_page_link" href="https://www.duolingo.com/errors/404.html">Go to Blank Page</a>
</div>
</div>
<div id="_footer">
<a href="https://twisk.fun/SuperLinkNexus" target="_blank">Superlinks Bot</a>
<a href="https://twisk.fun/" target="_blank">Our Website</a>
<a href="https://discord.gg/m3EV55SpYw" target="_blank">Discord</a>
<span>Version <span id="_version">1.0</span></span>
</div>
</div>
<div id="_floating_btn">🚀</div>
<div id="_popup_overlay"></div>
<div id="_popup_container">
<div id="_popup_header">
<span class="_popup_label">Join TWISK DEVELOPMENT!</span>
<button id="_popup_close_btn">✕</button>
</div>
<div id="_popup_body">
<div class="_popup_content">
<img src="https://discord.com/assets/2c21aeda9de2b3c44e2f.jpg" alt="Discord Logo" style="width: 80px; height: auto; margin-bottom: 16px;">
<p>Connect with TWISK DEVELOPMENT 🌐✅ on Discord! Join our server for updates, support, and a vibrant community.</p>
<a href="${DISCORD_LINK}" target="_blank" id="_join_discord_btn">Join Now!</a>
</div>
</div>
</div>
`;
const style = document.createElement("style");
style.innerHTML = `
body { font-family: 'Roboto', sans-serif; }
#_container {
width: 90vw;
max-width: 600px;
min-height: 50vh;
background: #ffffff;
color: #333333;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 20px rgba(33, 150, 243, 0.3);
display: flex;
flex-direction: column;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
overflow: hidden;
}
#_header {
height: 60px;
background: #2196f3;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
font-weight: 700;
position: relative;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
#_close_btn {
position: absolute;
right: 16px;
background: none;
border: none;
color: #ffffff;
font-size: 1.2em;
cursor: pointer;
}
#_body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
#_footer {
height: 50px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: space-around;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
font-size: 0.9em;
}
#_footer a, #_footer span {
color: #2196f3;
text-decoration: none;
font-weight: 500;
}
._card {
background: #f9f9f9;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
._card h3 {
margin: 0 0 12px 0;
font-size: 1.1em;
color: #2196f3;
font-weight: 500;
}
._info_row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 1em;
}
._info_row span:first-child {
color: #666666;
}
._info_row span:last-child {
font-weight: 500;
}
#_select_option {
width: 100%;
padding: 12px;
border: 1px solid #dddddd;
border-radius: 8px;
background: #ffffff;
color: #333333;
font-size: 1em;
margin-bottom: 12px;
}
._thread_row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
._thread_row label {
font-size: 0.9em;
color: #666666;
}
#_thread_count {
width: 60px;
padding: 8px;
border: 1px solid #dddddd;
border-radius: 8px;
text-align: center;
}
#_action_row {
display: flex;
gap: 12px;
}
#_start_btn, #_stop_btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
#_start_btn {
background: #2196f3;
color: #ffffff;
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);
}
#_start_btn:hover {
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.5);
}
#_stop_btn {
background: #f44336;
color: #ffffff;
box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
}
#_stop_btn:hover {
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.5);
}
._disable_btn {
background: #dddddd !important;
cursor: not-allowed !important;
box-shadow: none !important;
}
#_notify_card {
background: #e3f2fd;
color: #1976d2;
font-size: 0.9em;
}
#_blank_page_link {
color: #2196f3;
text-decoration: none;
font-weight: 500;
display: block;
margin-top: 8px;
}
#_overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
}
#_floating_btn {
position: fixed;
bottom: 5%;
right: 5%;
width: 56px;
height: 56px;
background: #2196f3;
color: #ffffff;
border-radius: 50%;
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4), 0 0 20px rgba(33, 150, 243, 0.6);
z-index: 10000;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5em;
transition: all 0.3s;
}
#_floating_btn:hover {
box-shadow: 0 6px 16px rgba(33, 150, 243, 0.6), 0 0 30px rgba(33, 150, 243, 0.8);
}
#_popup_container {
width: 90vw;
max-width: 400px;
background: #ffffff;
color: #333333;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2), 0 0 20px rgba(33, 150, 243, 0.3);
display: flex;
flex-direction: column;
position: fixed;
top: 30%;
left: 70%;
transform: translate(-50%, -50%);
z-index: 10001;
overflow: hidden;
}
#_popup_header {
height: 50px;
background: #2196f3;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
font-weight: 700;
position: relative;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
#_popup_close_btn {
position: absolute;
right: 12px;
background: none;
border: none;
color: #ffffff;
font-size: 1em;
cursor: pointer;
}
#_popup_body {
padding: 16px;
text-align: center;
}
._popup_content p {
margin: 0 0 16px 0;
font-size: 1em;
color: #333333;
}
#_join_discord_btn {
display: inline-block;
padding: 10px 20px;
background: #5865f2;
color: #ffffff;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s;
}
#_join_discord_btn:hover {
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.5);
}
#_popup_overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
z-index: 10000;
display: none;
}
.hidden { display: none; }
`;
document.head.appendChild(style);
const container = document.createElement("div");
container.innerHTML = containerHTML;
document.body.appendChild(container);
document.getElementById("_version").innerText = VERSION;
};
const setInterfaceVisible = (visible) => {
const container = document.getElementById("_container");
const overlay = document.getElementById("_overlay");
container.style.display = visible ? "flex" : "none";
overlay.style.display = visible ? "block" : "none";
};
const isInterfaceVisible = () => {
const container = document.getElementById("_container");
return container.style.display !== "none" && container.style.display !== "";
};
const toggleInterface = () => {
setInterfaceVisible(!isInterfaceVisible());
};
const setPopupVisible = (visible) => {
const popupContainer = document.getElementById("_popup_container");
const popupOverlay = document.getElementById("_popup_overlay");
popupContainer.style.display = visible ? "flex" : "none";
popupOverlay.style.display = visible ? "block" : "none";
};
const addEventFloatingBtn = () => {
const floatingBtn = document.getElementById("_floating_btn");
floatingBtn.addEventListener("click", toggleInterface);
};
const addEventCloseBtn = () => {
const closeBtn = document.getElementById("_close_btn");
closeBtn.addEventListener("click", () => setInterfaceVisible(false));
};
const addEventPopupCloseBtn = () => {
const popupCloseBtn = document.getElementById("_popup_close_btn");
popupCloseBtn.addEventListener("click", () => {
setPopupVisible(false);
setTimeout(() => setPopupVisible(true), REAPPEAR_DELAY);
});
};
const addEventStartBtn = () => {
const startBtn = document.getElementById("_start_btn");
const stopBtn = document.getElementById("_stop_btn");
const select = document.getElementById("_select_option");
const threadInput = document.getElementById("_thread_count");
startBtn.addEventListener("click", async () => {
isRunning = true;
startBtn.hidden = true;
stopBtn.hidden = false;
select.disabled = true;
threadInput.disabled = true;
threadCount = Math.max(parseInt(threadInput.value) || 1, 1);
rateLimitDelay = BASE_DELAY;
updateNotify(`Starting with ${threadCount} threads. User-agent rotation and adaptive delays activated.`);
const selected = select.options[select.selectedIndex];
const optionData = {
type: selected.getAttribute("data-type"),
amount: Number(selected.getAttribute("data-amount")),
from: selected.getAttribute("data-from"),
learn: selected.getAttribute("data-learn"),
value: selected.value,
label: selected.textContent,
};
await farmSelectedOption(optionData);
});
};
const addEventStopBtn = () => {
const startBtn = document.getElementById("_start_btn");
const stopBtn = document.getElementById("_stop_btn");
const select = document.getElementById("_select_option");
const threadInput = document.getElementById("_thread_count");
stopBtn.addEventListener("click", () => {
isRunning = false;
requestQueue = [];
activeRequests = 0;
rateLimitDelay = BASE_DELAY;
stopBtn.hidden = true;
startBtn.hidden = false;
select.disabled = false;
threadInput.disabled = false;
updateNotify("Farming stopped. Ready to start again.");
});
};
const addEventVersionLink = () => {
const versionLink = document.getElementById("_version");
versionLink.addEventListener("click", () => {
prompt("Your JWT token: ", jwt);
});
};
const addEventListeners = () => {
addEventFloatingBtn();
addEventCloseBtn();
addEventPopupCloseBtn();
addEventStartBtn();
addEventStopBtn();
addEventVersionLink();
};
const populateOptions = () => {
const select = document.getElementById("_select_option");
select.innerHTML = "";
const fromLang = userInfo?.fromLanguage || "ru";
const learnLang = userInfo?.learningLanguage || "en";
const options = [
{ type: "gem", label: `Gem 30`, value: `gem-30`, amount: 30 },
{
type: "xp",
label: `XP 499 (any -> en)`,
value: `xp-499`,
amount: 499,
from: fromLang,
learn: "en",
},
{
type: "streak",
label: `Streak repair (restore frozen streak)`,
value: `repair`,
},
{
type: "streak",
label: `Streak farm (beta test)`,
value: `farm`,
},
];
options.forEach((opt) => {
const option = document.createElement("option");
option.value = opt.value;
option.textContent = opt.label;
option.setAttribute("data-type", opt.type);
option.setAttribute("data-amount", opt.amount || 0);
option.setAttribute("data-from", opt.from || "");
option.setAttribute("data-learn", opt.learn || "");
select.appendChild(option);
});
};
const updateNotify = (message) => {
const notify = document.getElementById("_notify");
const now = new Date().toLocaleTimeString();
notify.innerText = `[${now}] ${message}`;
};
const disableInterface = (message = "") => {
const startBtn = document.getElementById("_start_btn");
const stopBtn = document.getElementById("_stop_btn");
const select = document.getElementById("_select_option");
const threadInput = document.getElementById("_thread_count");
startBtn.disabled = true;
stopBtn.disabled = true;
select.disabled = true;
threadInput.disabled = true;
if (message) updateNotify(message);
};
const resetStartStopBtn = () => {
isRunning = false;
requestQueue = [];
activeRequests = 0;
rateLimitDelay = BASE_DELAY;
const startBtn = document.getElementById("_start_btn");
const stopBtn = document.getElementById("_stop_btn");
const select = document.getElementById("_select_option");
const threadInput = document.getElementById("_thread_count");
stopBtn.hidden = true;
startBtn.hidden = false;
select.disabled = false;
threadInput.disabled = false;
};
const blockStopBtn = () => {
const stopBtn = document.getElementById("_stop_btn");
stopBtn.disabled = true;
stopBtn.classList.add("_disable_btn");
};
const unblockStopBtn = () => {
const stopBtn = document.getElementById("_stop_btn");
stopBtn.disabled = false;
stopBtn.classList.remove("_disable_btn");
};
//--------------------Logic--------------------//
const getJwtToken = () => {
var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.startsWith("jwt_token=")) {
return cookie.substring("jwt_token=".length);
}
}
return null;
};
const decodeJwtToken = (token) => {
var base64Url = token.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
var jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
return JSON.parse(jsonPayload);
};
const getRandomUserAgent = () => {
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
};
const formatHeaders = (jwt) => ({
"Content-Type": "application/json",
Authorization: "Bearer " + jwt,
"User-Agent": getRandomUserAgent(),
});
const getUserInfo = async (sub) => {
const userInfoUrl = `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData`;
let response = await fetch(userInfoUrl, {
method: "GET",
headers: defaultHeaders,
});
return await response.json();
};
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const adjustRateLimitDelay = (status) => {
if (status === 429) {
rateLimitDelay = Math.min(rateLimitDelay * 2, 15000);
updateNotify(`Rate limit hit! Increasing delay to ${rateLimitDelay}ms.`);
} else {
rateLimitDelay = Math.max(BASE_DELAY, rateLimitDelay * 0.75);
updateNotify(`Successful request. Current delay: ${rateLimitDelay}ms.`);
}
};
const processQueue = async () => {
while (requestQueue.length > 0 && activeRequests < MAX_CONCURRENT_REQUESTS && isRunning) {
const { task, resolve, reject } = requestQueue.shift();
activeRequests++;
try {
const result = await task();
resolve(result);
await delay(rateLimitDelay + Math.random() * 200);
} catch (error) {
reject(error);
} finally {
activeRequests--;
processQueue();
}
}
};
const queueRequest = (task) => {
return new Promise((resolve, reject) => {
requestQueue.push({ task, resolve, reject });
processQueue();
});
};
const updateUserInfo = () => {
const username = document.getElementById("_username");
const from = document.getElementById("_from");
const learn = document.getElementById("_learn");
const streak = document.getElementById("_streak");
const gem = document.getElementById("_gem");
const xp = document.getElementById("_xp");
username.innerText = userInfo.username;
from.innerText = userInfo.fromLanguage;
learn.innerText = userInfo.learningLanguage;
streak.innerText = userInfo.streak;
gem.innerText = userInfo.gems;
xp.innerText = userInfo.totalXp;
};
const toTimestamp = (dateStr) => {
return Math.floor(new Date(dateStr).getTime() / 1000);
};
const daysBetween = (startTimestamp, endTimestamp) => {
return Math.floor((endTimestamp - startTimestamp) / (60 * 60 * 24));
};
const sendRequest = async ({ url, payload, headers, method = "PUT" }) => {
try {
const res = await fetch(url, {
method,
headers,
body: payload ? JSON.stringify(payload) : undefined,
});
adjustRateLimitDelay(res.status);
if (res.status === 429) {
await delay(rateLimitDelay);
return sendRequest({ url, payload, headers, method });
}
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res;
} catch (error) {
adjustRateLimitDelay(429);
throw error;
}
};
const sendRequestWithDefaultHeaders = async ({
url,
payload,
headers = {},
method = "GET",
}) => {
defaultHeaders = formatHeaders(jwt);
const mergedHeaders = { ...defaultHeaders, ...headers };
return sendRequest({ url, payload, headers: mergedHeaders, method });
};
const farmGemOnce = async () => {
const idReward =
"SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
const patchUrl = `https://www.duolingo.com/2017-06-30/users/${sub}/rewards/${idReward}`;
const patchData = {
consumed: true,
learningLanguage: userInfo.learningLanguage,
fromLanguage: userInfo.fromLanguage,
};
return await queueRequest(() =>
sendRequestWithDefaultHeaders({
url: patchUrl,
payload: patchData,
method: "PATCH",
})
);
};
const farmGemLoop = async () => {
const gemFarmed = 30;
while (isRunning) {
try {
const promises = Array.from({ length: threadCount }, () => farmGemOnce());
await Promise.all(promises);
userInfo = { ...userInfo, gems: userInfo.gems + gemFarmed * threadCount };
updateNotify(`You got ${gemFarmed * threadCount} gems with ${threadCount} threads!!!`);
updateUserInfo();
await delay(rateLimitDelay);
} catch (error) {
updateNotify(
`Error ${error.message}! Please record screen and report in telegram group!`
);
await delay(rateLimitDelay + 1000);
}
}
};
const farmXpOnce = async (amount) => {
const startTime = Math.floor(Date.now() / 1000);
const fromLanguage = userInfo.fromLanguage;
const completeUrl = `https://stories.duolingo.com/api2/stories/en-${fromLanguage}-the-passport/complete`;
const payload = {
awardXp: true,
isFeaturedStoryInPracticeHub: false,
completedBonusChallenge: true,
mode: "READ",
isV2Redo: false,
isV2Story: false,
isLegendaryMode: true,
masterVersion: false,
maxScore: 0,
numHintsUsed: 0,
score: 0,
startTime: startTime,
fromLanguage: fromLanguage,
learningLanguage: "en",
hasXpBoost: false,
happyHourBonusXp: 449,
};
return await queueRequest(() =>
sendRequestWithDefaultHeaders({
url: completeUrl,
payload: payload,
headers: defaultHeaders,
method: "POST",
})
);
};
const farmXpLoop = async (amount) => {
while (isRunning) {
try {
const promises = Array.from({ length: threadCount }, () => farmXpOnce(amount));
const responses = await Promise.all(promises);
let totalXpFarmed = 0;
for (const response of responses) {
if (response.status === 500) {
updateNotify(
"Make sure you are on English course (learning lang must be EN)!"
);
await delay(rateLimitDelay + 1000);
continue;
}
const responseData = await response.json();
totalXpFarmed += responseData?.awardedXp || 0;
}
userInfo = { ...userInfo, totalXp: userInfo.totalXp + totalXpFarmed };
updateNotify(`You got ${totalXpFarmed} XP with ${threadCount} threads!!!`);
updateUserInfo();
await delay(rateLimitDelay);
} catch (error) {
updateNotify(
`Error ${error.message}! Please record screen and report in telegram group!`
);
await delay(rateLimitDelay + 1000);
}
}
};
const farmSessionOnce = async (startTime, endTime) => {
const sessionPayload = {
challengeTypes: [
"assist",
"characterIntro",
"characterMatch",
"characterPuzzle",
"characterSelect",
"characterTrace",
"characterWrite",
"completeReverseTranslation",
"definition",
"dialogue",
"extendedMatch",
"extendedListenMatch",
"form",
"freeResponse",
"gapFill",
"judge",
"listen",
"listenComplete",
"listenMatch",
"match",
"name",
"listenComprehension",
"listenIsolation",
"listenSpeak",
"listenTap",
"orderTapComplete",
"partialListen",
"partialReverseTranslate",
"patternTapComplete",
"radioBinary",
"radioImageSelect",
"radioListenMatch",
"radioListenRecognize",
"radioSelect",
"readComprehension",
"reverseAssist",
"sameDifferent",
"select",
"selectPronunciation",
"selectTranscription",
"svgPuzzle",
"syllableTap",
"syllableListenTap",
"speak",
"tapCloze",
"tapClozeTable",
"tapComplete",
"tapCompleteTable",
"tapDescribe",
"translate",
"transliterate",
"transliterationAssist",
"typeCloze",
"typeClozeTable",
"typeComplete",
"typeCompleteTable",
"writeComprehension",
],
fromLanguage: userInfo.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: userInfo.learningLanguage,
smartTipsVersion: 2,
type: "GLOBAL_PRACTICE",
};
const sessionRes = await queueRequest(() =>
sendRequestWithDefaultHeaders({
url: "https://www.duolingo.com/2017-06-30/sessions",
payload: sessionPayload,
method: "POST",
})
);
const sessionData = await sessionRes.json();
const updateSessionPayload = {
...sessionData,
heartsLeft: 0,
startTime: startTime,
enableBonusPoints: false,
endTime: endTime,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
};
return await queueRequest(() =>
sendRequestWithDefaultHeaders({
url: `https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`,
payload: updateSessionPayload,
method: "PUT",
})
);
};
const repairStreak = async () => {
blockStopBtn();
try {
if (!userInfo.streakData.currentStreak) {
updateNotify("You have no streak! Abort!");
resetStartStopBtn();
return;
}
const startStreakDate = userInfo.streakData.currentStreak.startDate;
const endStreakDate = userInfo.streakData.currentStreak.endDate;
const startStreakTimestamp = toTimestamp(startStreakDate);
const endStreakTimestamp = toTimestamp(endStreakDate);
const expectedStreak = daysBetween(startStreakTimestamp, endStreakTimestamp) + 1;
if (expectedStreak > userInfo.streak) {
updateNotify("Your streak is frozen somewhere! Repairing...");
await delay(rateLimitDelay);
let currentTimestamp = Math.floor(Date.now() / 1000);
const repairPromises = [];
for (let i = 0; i < expectedStreak; i++) {
repairPromises.push(farmSessionOnce(currentTimestamp, currentTimestamp + 60));
currentTimestamp -= 86400;
}
await Promise.all(repairPromises.map((p, i) => p.then(() => updateNotify(`Repaired streak (${i + 1}/${expectedStreak})...`))));
const userAfterRepair = await getUserInfo(sub);
if (userAfterRepair.streakData.currentStreak.length > expectedStreak) {
updateNotify(`Your streak has been repaired! No more frozen streak!`);
userInfo = userAfterRepair;
updateUserInfo();
} else {
updateNotify(`Streak repair failed or no frozen streak! Please check your account!`);
}
} else {
updateNotify("You have no frozen streak! No need to repair!");
resetStartStopBtn();
return;
}
} finally {
unblockStopBtn();
}
};
const farmStreakLoop = async () => {
const hasStreak = !!userInfo.streakData.currentStreak;
const startStreakDate = hasStreak
? userInfo.streakData.currentStreak.startDate
: new Date();
const startFarmStreakTimestamp = toTimestamp(startStreakDate);
let currentTimestamp = hasStreak
? startFarmStreakTimestamp - 86400
: startFarmStreakTimestamp;
while (isRunning) {
try {
const sessionRes = await farmSessionOnce(currentTimestamp, currentTimestamp + 60);
if (sessionRes) {
currentTimestamp -= 86400;
userInfo = { ...userInfo, streak: userInfo.streak + 1 };
updateNotify(`You got +1 streak!`);
updateUserInfo();
await delay(rateLimitDelay);
} else {
updateNotify("Failed to farm streak session, trying again...");
await delay(rateLimitDelay + 2000);
continue;
}
} catch (error) {
updateNotify(`Error in farmStreak: ${error?.message || error}`);
await delay(rateLimitDelay + 2000);
continue;
}
}
};
const farmSelectedOption = async (option) => {
const { type, value, amount } = option;
switch (type) {
case "gem":
await farmGemLoop();
break;
case "xp":
await farmXpLoop(amount);
break;
case "streak":
if (value === "repair") {
await repairStreak();
} else if (value === "farm") {
await farmStreakLoop();
}
break;
}
resetStartStopBtn();
};
const initVariables = async () => {
jwt = getJwtToken();
if (!jwt) {
disableInterface("Please login to Duolingo and reload!");
return;
}
defaultHeaders = formatHeaders(jwt);
const decodedJwt = decodeJwtToken(jwt);
sub = decodedJwt.sub;
userInfo = await getUserInfo(sub);
populateOptions();
};
(async () => {
initInterface();
setInterfaceVisible(false);
setPopupVisible(true);
addEventListeners();
await initVariables();
updateUserInfo();
})();