Hacklingo

Hacklingo — tool for farming XP, streaks, and gems with ultra-fast multi-thread support.

// ==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();
})();