Greasy Fork

Greasy Fork is available in English.

DuoFarmer

[ LITE ] DuoFarmer 是一款可帮助您以惊人的速度在 Duolingo 中赚取 XP 的工具。

当前为 2025-07-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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="http://greasyfork.icu/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();
})();