您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[ LITE ] DuoFarmer 是一款可帮助您以惊人的速度在 Duolingo 中赚取 XP 的工具。
// ==UserScript== // @name DuoFarmer // @namespace https://duo-farmer.vercel.app // @version 1.2 // @description [ LITE ] DuoFarmer is a tool that helps you earn XP in Duolingo at blazing speed. // @description:vi [ LITE ] DuoFarmer là một công cụ giúp bạn hack XP trong Duolingo với tốc độ cực nhanh. // @description:fr [ LITE ] DuoFarmer est un outil qui vous aide à gagner de l'XP sur Duolingo à une vitesse fulgurante. // @description:es [ LITE ] DuoFarmer es una herramienta que te ayuda a ganar XP en Duolingo a una velocidad asombrosa. // @description:de [ LITE ] DuoFarmer ist ein Tool, das Ihnen hilft, in Duolingo blitzschnell XP zu verdienen. // @description:it [ LITE ] DuoFarmer è uno strumento che ti aiuta a guadagnare XP su Duolingo a una velocità incredibile. // @description:ja [ LITE ] DuoFarmer は、Duolingo で驚異的なスピードで XP を獲得するのに役立つツールです。 // @description:ko [ LITE ] DuoFarmer는 Duolingo에서 엄청난 속도로 XP를 얻을 수 있도록 도와주는 도구입니다. // @description:ru [ LITE ] DuoFarmer - это инструмент, который помогает вам зарабатывать XP в Duolingo с невероятной скоростью. // @description:zh-CN [ LITE ] DuoFarmer 是一款可帮助您以惊人的速度在 Duolingo 中赚取 XP 的工具。 // @author Lamduck // @match https://*.duolingo.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=duolingo.com // @grant none // @license none // ==/UserScript== const VERSION = "1.2"; const DELAY = 500; var jwt, defaultHeaders, userInfo, sub; let isRunning = false; const initInterface = () => { const containerHTML = ` <link rel="stylesheet" href="xp.css" /> <div id="_overlay"></div> <div id="_container"> <div id="_header"> <span class="_label">Duofarmer minimal UI</span> </div> <div id="_body"> <table id="_table_main" class="_table"> <thead> <tr> <th>Username</th> <th>From</th> <th>Learning</th> </tr> </thead> <tbody> <tr> <td id="_username">duofarmer</td> <td id="_from">any</td> <td id="_learn">any</td> </tr> </tbody> </table> <table id="_table_progress" class="_table"> <thead> <tr> <th>Streak</th> <th>Gem</th> <th>XP</th> </tr> </thead> <tbody> <tr> <td id="_streak">0</td> <td id="_gem">0</td> <td id="_xp">0</td> </tr> </tbody> </table> <div id="_action_row"> <select id="_select_option"> <!-- <option value="option1">Option 1</option> --> <!-- <option value="option2">Option 2</option> --> </select> <button id="_start_btn">Start</button> <button id="_stop_btn" hidden>Stop</button> </div> <div id="_notify"> This is a minimal version that reduces device resources. <br /> Only support learning language is English. <br /> (will not work if from language is also English) <br> <br> Recommended to go to blank page for max performance + reduce resource usage. </div> <a id="_blank_page_link" href="https://www.duolingo.com/errors/404.html">Blank page (click here)</a> </div> <div id="_footer"> <a href="https://greasyfork.org/vi/scripts/528621-duofarmer" target="_blank" >Greasyfork</a > <a href="https://t.me/duofarmer" target="_blank">Telegram</a> <a>Version <span id="_version">1.0</span></a> </div> </div> <div id="_floating_btn">😼</div> `; const style = document.createElement("style"); style.innerHTML = `#_container { width: 90vw; max-width: 800px; min-height: 40vh; max-height: 90vh; background: #222; color: #fff; border-radius: 10px; box-shadow: 0 2px 12px #0008; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; } #_header { height: 60px; background: #333; display: flex; align-items: center; justify-content: center; border-top-left-radius: 10px; border-top-right-radius: 10px; width: 100%; } #_body { min-height: 40vh; max-height: 100%; min-width: 0; background: #282828; display: flex; align-items: center; justify-content: center; width: 100%; overflow-y: auto; flex: 1; flex-direction: column; } #_footer { height: 50px; background: #222; display: flex; align-items: center; justify-content: space-evenly; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; width: 100%; } ._label { font-size: 1em; } #_header ._label { font-size: 1.5em; font-style: italic; font-weight: bold; color: #fac8ff; } #_body ._label { font-size: 1.2em; } ._table { width: 100%; background: #232323; color: #fff; border-radius: 8px; padding: 8px 12px; text-align: center; table-layout: fixed; } ._table th, ._table td { padding: 9px 12px; text-align: center; border-bottom: 1px solid #444; width: 1%; } ._table tbody tr:last-child td { border-bottom: none; } #_body h3 { margin: 0; color: #fff; font-size: 1.1em; font-weight: bold; letter-spacing: 1px; } #_action_row { width: 90%; display: flex; justify-content: space-between; align-items: center; margin: 8px 0; gap: 8px; } #_select_option { width: 90%; max-width: 90%; margin-right: 8px; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #232323; color: #fff; font-size: 1em; outline: none; } #_start_btn, #_stop_btn { width: auto; margin-left: 0; padding: 8px 18px; border-radius: 6px; border: none; background: #229100; color: #fff; font-size: 1em; font-weight: bold; cursor: pointer; box-shadow: 0 2px 8px #0003; } #_stop_btn { background: #af0303; } ._disable_btn { background: #52454560 !important; cursor: not-allowed !important; } #_notify { width: 90%; max-width: 90%; min-height: 10vh; margin: 8px 0; padding: 8px 12px; border-radius: 6px; background: #333; color: #c8ff00; font-size: 1em; word-wrap: break-word; /* text-align: center; */ } #_blank_page_link { margin-bottom: 8px; color: #fce6ff; font-weight: bold; font-style: italic; } #_footer a, #_footer span { text-decoration: none; color: #00aeff; font-size: 1em; font-weight: bold; font-style: italic; } #_overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); z-index: 9998; pointer-events: all; } #_floating_btn { position: fixed; bottom: 10%; right: 2%; width: 40px; height: 40px; background: #35bd00; border-radius: 50%; box-shadow: 0 2px 8px #0005; z-index: 10000; cursor: pointer; display: flex; align-items: center; justify-content: center; } .hidden{ display: none; }`; document.head.appendChild(style); const container = document.createElement("div"); container.innerHTML = containerHTML; document.body.appendChild(container); // doi phien ban const version = document.getElementById("_version"); 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 addEventFloatingBtn = () => { const floatingBtn = document.getElementById("_floating_btn"); const startBtn = document.getElementById("_start_btn"); const stopBtn = document.getElementById("_stop_btn"); const select = document.getElementById("_select_option"); floatingBtn.addEventListener("click", () => { if (isRunning) { if (confirm("Duofarmer is farming. Do you want to stop and hide UI?")) { isRunning = false; stopBtn.hidden = true; startBtn.hidden = false; startBtn.disabled = true; startBtn.className = "_disable_btn"; select.disabled = false; setTimeout(() => { startBtn.className = ""; startBtn.disabled = false; }, 2000); setInterfaceVisible(false); return; } else { // Không làm gì nếu không muốn dừng return; } } setInterfaceVisible(!isInterfaceVisible()); }); }; const addEventStartBtn = () => { const startBtn = document.getElementById("_start_btn"); const stopBtn = document.getElementById("_stop_btn"); const select = document.getElementById("_select_option"); startBtn.addEventListener("click", async () => { isRunning = true; startBtn.hidden = true; stopBtn.hidden = false; stopBtn.disabled = true; stopBtn.className = "_disable_btn"; select.disabled = true; // Lấy option đang chọn 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); setTimeout(() => { stopBtn.className = ""; stopBtn.disabled = false; }, 2000); }); }; const addEventStopBtn = () => { const startBtn = document.getElementById("_start_btn"); const stopBtn = document.getElementById("_stop_btn"); const select = document.getElementById("_select_option"); stopBtn.addEventListener("click", () => { isRunning = false; stopBtn.hidden = true; startBtn.hidden = false; startBtn.disabled = true; startBtn.className = "_disable_btn"; select.disabled = false; setTimeout(() => { startBtn.className = ""; startBtn.disabled = false; }, 2000); }); }; const addEventVersionLink = () => { const versionLink = document.getElementById("_version"); versionLink.addEventListener("click", () => { prompt("Your JWT token: ", jwt); }); }; const addEventListeners = () => { addEventFloatingBtn(); addEventStartBtn(); addEventStopBtn(); addEventVersionLink(); }; // Thêm hàm tạo option cho select const populateOptions = () => { const select = document.getElementById("_select_option"); select.innerHTML = ""; // Lấy fromLanguage và learningLanguage từ userInfo nếu có const fromLang = userInfo?.fromLanguage || "ru"; const learnLang = userInfo?.learningLanguage || "en"; // Danh sách option mẫu 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); 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 = (notify = {}) => { const startBtn = document.getElementById("_start_btn"); const stopBtn = document.getElementById("_stop_btn"); const select = document.getElementById("_select_option"); startBtn.disabled = true; startBtn.className = "_disable_btn"; stopBtn.disabled = true; select.disabled = true; if (notify) { const notifyElement = document.getElementById("_notify"); notifyElement.innerText = notify; } }; const resetStartStopBtn = () => { isRunning = false; const startBtn = document.getElementById("_start_btn"); const stopBtn = document.getElementById("_stop_btn"); const select = document.getElementById("_select_option"); stopBtn.hidden = true; startBtn.hidden = false; startBtn.disabled = true; startBtn.className = "_disable_btn"; select.disabled = false; setTimeout(() => { startBtn.className = ""; startBtn.disabled = false; }, 2000); }; 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 formatHeaders = (jwt) => ({ "Content-Type": "application/json", Authorization: "Bearer " + jwt, "User-Agent": navigator.userAgent, }); 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 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, }); return res; } catch (error) { return error; } }; const sendRequestWithDefaultHeaders = async ({ url, payload, headers = {}, method = "GET", }) => { 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 sendRequestWithDefaultHeaders({ url: patchUrl, payload: patchData, method: "PATCH", }); }; const farmGemLoop = async () => { const gemFarmed = 30; while (isRunning) { try { await farmGemOnce(); userInfo = { ...userInfo, gems: userInfo.gems + gemFarmed }; updateNotify(`You got ${gemFarmed} gem!!!`); updateUserInfo(); await delay(DELAY); } catch (error) { updateNotify( `Error ${error.status}! Please record screen and report in telegram group!` ); await delay(DELAY + 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 sendRequestWithDefaultHeaders({ url: completeUrl, payload: payload, headers: defaultHeaders, method: "POST", }); }; const farmXpLoop = async (amount) => { while (isRunning) { try { const response = await farmXpOnce(amount); if (response.status == 500) { updateNotify( "Make sure you are on English course (learning lang must be EN)!" ); await delay(DELAY + 1000); continue; } const responseData = await response.json(); const xpFarmed = responseData?.awardedXp || 0; userInfo = { ...userInfo, totalXp: userInfo.totalXp + xpFarmed }; updateNotify(`You got ${xpFarmed} XP!!!`); updateUserInfo(); await delay(DELAY); } catch (error) { updateNotify( `Error ${error.status}! Please record screen and report in telegram group!` ); await delay(DELAY + 1000); } } }; const farmSessionOnce = async (startTime, endTime) => { //tạo và lấy session 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 sendRequestWithDefaultHeaders({ url: "https://www.duolingo.com/2017-06-30/sessions", payload: sessionPayload, method: "POST", }); const sessionData = await sessionRes.json(); // lấy session và gán vào update const updateSessionPayload = { ...sessionData, heartsLeft: 0, startTime: startTime, enableBonusPoints: false, endTime: endTime, failed: false, maxInLessonStreak: 9, shouldLearnThings: true, }; const updateRes = await sendRequestWithDefaultHeaders({ url: `https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, payload: updateSessionPayload, method: "PUT", }); return await updateRes.json(); }; const repairStreak = async () => { blockStopBtn(); try { if(!userInfo.streakData.currentStreak) { updateNotify("You have no streak! Abort!"); resetStartStopBtn(); } 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; //+1 vì nó bị lệch gì đó //check xem có streak freeze không if (expectedStreak > userInfo.streak) { updateNotify("Your streak is frozen somewhere! Repairing..."); await delay(2000); let currentTimestamp = Math.floor(Date.now() / 1000); for (let i = 0; i < expectedStreak; i++) { const createdSession = await farmSessionOnce( currentTimestamp, currentTimestamp + 60 ); currentTimestamp -= 86400; updateNotify( `Trying to repair streak ( ${i + 1}/${expectedStreak})...` ); await delay(DELAY); } 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 // Nếu đã có streak, farm từ ngày trước đó : startFarmStreakTimestamp; // Nếu chưa có streak, farm luôn ngày hôm nay 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(DELAY); } else { updateNotify("Failed to farm streak session, I'm trying again..."); await delay(2000); continue; } } catch (error) { updateNotify(`Error in farmStreak: ${error?.message || error}`); await delay(2000); continue; } } } const farmSelectedOption = async (option) => { const { type, value, amount, from, learn } = option; switch (type) { case "gem": farmGemLoop(); break; case "xp": farmXpLoop(amount); break; case "streak": if (value == "repair") { repairStreak(); } else if (value == "farm") { farmStreakLoop(); } break; } }; 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(); // cập nhật lại option khi đã có userInfo }; //--------------------Main--------------------// (async () => { initInterface(); setInterfaceVisible(false); addEventListeners(); await initVariables(); updateUserInfo(); })();