Duolingo DuoFarmer

DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.

Zainstaluj skrypt?
Skrypt zaproponowany przez autora

Może Ci się również spodobać. Get Duolingo JWT

Zainstaluj skrypt
// ==UserScript==
// @name         Duolingo DuoFarmer
// @namespace    https://duo-farmer.vercel.app
// @version      1.3.7
// @author       Lamduck
// @description  DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.
// @license      none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// @match        https://*.duolingo.com/*
// @grant        GM_log
// ==/UserScript==

(function () {
  'use strict';

  const templateRaw = `<div id="overlay"></div>
<div id="container">
  <div id="header">
    <span class="label">Duofarmer</span>
    <button id="settings-btn">⚙️</button>
  </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">Getting user info, please wait until it's done.<br /> If it takes too long, please refresh the
      page.</div>
  </div>
  <div id="footer">
    <span class="label">u gay 💔</span>
  </div>
</div>
<div id="settings-container">
  <div id="settings-menu" class="modal-content">
    <div class="modal-header">
      <span class="label">Settings</span>
      <button id="settings-close" class="modal-close">✕</button>
    </div>
    <div class="modal-body">
      <div class="settings-group">
        <h3>General</h3>
        <div class="setting-item">
          <span>Auto open UI onload</span>
          <input type="checkbox" id="auto-open-ui">
        </div>
        <div class="setting-item">
          <span>Auto start farming onload</span>
          <input type="checkbox" id="auto-start">
        </div>
        <div class="setting-item">
          <span>Default farming option</span>
          <select id="default-option">
            <!-- option auto -->
          </select>
        </div>
        <div class="setting-item">
          <span>Hide username</span>
          <input type="checkbox" id="hide-username">
        </div>
        <div class="setting-item">
          <span>Keep screen on</span>
          <input type="checkbox" id="keep-screen-on">
        </div>
      </div>
      <div class="settings-group">
        <h3>Performance</h3>
        <div class="setting-item">
          <span>Delay time (100ms - 10000ms):
            <br>
            <i class="muted">(Lower delay = faster = high limit rate ban)</i>
          </span>
          <input type="number" id="delay-time" min="100" max="10000" value="500">
        </div>
        <div class="setting-item">
          <span>Retry time (100ms - 10000ms):
            <br>
          </span>
          <input type="number" id="retry-time" min="100" max="10000" value="1000">
        </div>
        <div class="setting-item">
          <span>Auto stop after (min) (set 0 for unlimited)</span>
          <input type="number" id="auto-stop-time" min="0" max="60" value="0">
        </div>
      </div>
      <div class="settings-group disabled">
        <h3>Interface (coming soon)</h3>
        <div class="setting-item">
          <span>Dark mode</span>
          <input type="checkbox" id="dark-mode">
        </div>
        <div class="setting-item">
          <span>Compact UI</span>
          <input type="checkbox" id="compact-ui">
        </div>
        <div class="setting-item">
          <span>Show progress bar</span>
          <input type="checkbox" id="show-progress">
        </div>
        <div class="setting-item">
          <span>Font size</span>
          <select id="font-size">
            <option value="small">Small</option>
            <option value="medium">Medium</option>
            <option value="large">Large</option>
          </select>
        </div>
        <div class="setting-item">
          <span>Reset theme</span>
          <button id="reset-theme" class="setting-btn">Reset</button>
        </div>
      </div>
      <div class="settings-group">
        <h3>Advanced</h3>
        <div class="setting-item">
          <span>Get ur JWT token</span>
          <button id="get-jwt-token" class="setting-btn">Get Token</button>
        </div>
        <div class="setting-item">
          <span>Set account to public</span>
          <button id="set-account-public" class="setting-btn">Set Public</button>
        </div>
        <div class="setting-item">
          <span>Set account to private</span>
          <button id="set-account-private" class="setting-btn">Set Private</button>
        </div>
        <div class="setting-item">
          <span>Quick logout</span>
          <button id="quick-logout" class="setting-btn">Logout</button>
        </div>
        <div class="setting-item">
          <span>Reset setting</span>
          <button id="reset-setting" class="setting-btn">Reset</button>
        </div>
      </div>
      <div class="settings-group">
        <h3>Others</h3>
        <div class="setting-item">
          <span>Blank page (best performance)</span>
          <a href="https://www.duolingo.com/errors/0">Here</a>
        </div>
        <div class="setting-item">
          <span>Duolingo homepage</span>
          <a href="https://www.duolingo.com/">Here</a>
        </div>
        <div class="setting-item">
          <span>Greasyfork</span>
          <a href="https://greasyfork.org/vi/scripts/528621-duofarmer" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>Telegram</span>
          <a href="https://t.me/duofarmer" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>Duofarmer Homepage</span>
          <a href="https://duo-farmer.vercel.app" target="_blank">Here</a>
        </div>
        <div class="setting-item">
          <span>All user info:
            <br>
            <code id="user-info-display">
              Loading...
            </code>
          </span>
        </div>

      </div>
    </div>
    <div class="modal-footer">
      <span></span>
      <button id="save-settings" class="save-btn">Save</button>
    </div>
  </div>
</div>
<div id="floating-btn">🐸</div>`;
  const cssText = "#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;font-size:.9em;display:flex;flex-direction:column;align-items:center;justify-content:center;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:9999;box-sizing:border-box}#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%;position:relative}#settings-btn{position:absolute;right:20px;background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:5px;border-radius:3px}#settings-btn:hover{background:#555}#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:30px;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:700;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:700;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:700;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}#blank-page-link{margin-bottom:8px;color:#fce6ff;font-weight:700;font-style:italic}#footer a,#footer span{text-decoration:none;color:#00aeff;font-size:1em;font-weight:700;font-style:italic}#overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000c;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 #0000004d;z-index:10000;cursor:pointer;display:flex;align-items:center;justify-content:center}#settings-container{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10000;display:flex;align-items:center;justify-content:center;background:#000c}.modal-content{width:90vw;max-width:800px;min-height:25vh;max-height:70vh;background:#222;color:#fff;border-radius:10px;box-shadow:0 2px 12px #0008;font-family:sans-serif;font-size:.9em;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;box-sizing:border-box}.modal-header{height:60px;background:#333;display:flex;align-items:center;justify-content:space-between;padding:0 20px;border-top-left-radius:10px;border-top-right-radius:10px;width:100%}.modal-header .label{font-weight:700}.modal-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;padding:5px;border-radius:3px}.modal-close:hover{background:#555}.modal-body{min-height:25vh;max-height:100%;min-width:0;background:#282828;display:flex;align-items:center;justify-content:flex-start;width:100%;overflow-y:auto;flex:1;flex-direction:column;padding:20px}.modal-footer{height:60px;background:#333;display:flex;align-items:center;justify-content:flex-end;padding:5px 20px;border-top:1px solid #444;border-bottom-left-radius:10px;border-bottom-right-radius:10px;width:100%}.save-btn{padding:8px 10px;border-radius:6px;border:none;background:#229100;color:#fff;font-weight:bolder;cursor:pointer}.settings-group{width:100%;margin-bottom:30px}.settings-group h3{margin:0 0 15px;color:#fff;font-size:16px;border-bottom:1px solid #444;padding-bottom:5px}.setting-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px;padding:10px;background:#333;border-radius:5px}.setting-item span{flex:1;margin-right:10px}.setting-item input[type=checkbox]{width:18px;height:18px;margin-left:auto;cursor:pointer;accent-color:#229100}.setting-item input[type=number]{width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;text-align:center;outline:none}.setting-item input[type=number]:focus{border-color:#229100}.setting-item input:not([type=checkbox]){width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;outline:none}.setting-item select{width:120px;padding:8px 12px;margin-left:auto;border-radius:6px;border:1px solid #444;background:#232323;color:#fff;font-size:1em;outline:none;cursor:pointer}.setting-item .setting-btn{padding:6px 12px;margin-left:auto;background:#555;border:1px solid #666;border-radius:4px;color:#fff;font-size:.9em;cursor:pointer}.setting-item a{color:#4caf50;font-style:italic;text-decoration:none;margin-left:auto;font-size:.9em}.setting-item a:hover{color:#66bb6a;text-decoration:underline}.blur{filter:blur(4px)}.disabled{background:#26202060!important;color:#888!important;cursor:not-allowed!important;pointer-events:none!important}.hidden{display:none!important}.muted{color:#555!important;font-size:smaller!important}code{background:#333;margin:10px 0;padding:8px 12px;border-left:#229100 3px solid;font-family:monospace;line-height:1.5em;display:block;border-radius:2px}";
  const log = (message) => {
    if (typeof GM_log !== "undefined") {
      GM_log(message);
    } else {
      console.log("[DuoFarmer]", message);
    }
  };
  const logError = (error, context = "") => {
    const message = (error == null ? void 0 : error.message) || (error == null ? void 0 : error.toString()) || "Unknown error";
    const fullMessage = context ? `[${context}] ${message}` : message;
    log(fullMessage);
  };
  const delay = (ms) => {
    return new Promise((resolve) => setTimeout(resolve, ms));
  };
  const toTimestamp = (dateStr) => {
    return Math.floor(new Date(dateStr).getTime() / 1e3);
  };
  const getCurrentUnixTimestamp = () => {
    return Math.floor(Date.now() / 1e3);
  };
  const getJwtToken = () => {
    const cookies = document.cookie.split(";");
    for (let i = 0; i < cookies.length; i++) {
      const cookie = cookies[i].trim();
      if (cookie.startsWith("jwt_token=")) {
        return cookie.substring("jwt_token=".length);
      }
    }
    return null;
  };
  const decodeJwtToken = (token) => {
    const base64Url = token.split(".")[1];
    const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
    const jsonPayload = decodeURIComponent(
      atob(base64).split("").map(function(c) {
        return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
      }).join("")
    );
    return JSON.parse(jsonPayload);
  };
  const formatHeaders = (jwtToken) => {
    return {
      "Content-Type": "application/json",
      Authorization: `Bearer ${jwtToken}`,
      "User-Agent": navigator.userAgent
    };
  };
  const extractSkillId = (currentCourse) => {
    var _a, _b;
    const sections = (currentCourse == null ? void 0 : currentCourse.pathSectioned) || [];
    for (const section of sections) {
      const units = section.units || [];
      for (const unit of units) {
        const levels = unit.levels || [];
        for (const level of levels) {
          const skillId2 = ((_a = level.pathLevelMetadata) == null ? void 0 : _a.skillId) || ((_b = level.pathLevelClientData) == null ? void 0 : _b.skillId);
          if (skillId2) return skillId2;
        }
      }
    }
    return null;
  };
  class ApiService {
    constructor(jwt2, defaultHeaders2, userInfo2, sub2) {
      this.jwt = jwt2;
      this.defaultHeaders = defaultHeaders2;
      this.userInfo = userInfo2;
      this.sub = sub2;
    }
    static async getUserInfo(userSub, headers) {
      const userInfoUrl = `https://www.duolingo.com/2017-06-30/users/${userSub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData,privacySettings,currentCourse{pathSectioned{units{levels{pathLevelMetadata{skillId}}}}}`;
      const response = await fetch(userInfoUrl, { method: "GET", headers });
      return await response.json();
    }
    async sendRequest({ url, payload, headers, method = "PUT" }) {
      try {
        const res = await fetch(url, {
          method,
          headers,
          body: payload ? JSON.stringify(payload) : void 0
        });
        return res;
      } catch (error) {
        return error;
      }
    }
    async setPrivacyStatus(privacyStatus) {
      const patchUrl = `https://www.duolingo.com/2017-06-30/users/${this.sub}/privacy-settings?fields=privacySettings`;
      const patchBody = {
        "DISABLE_SOCIAL": privacyStatus
      };
      return await this.sendRequest({ url: patchUrl, payload: patchBody, headers: this.defaultHeaders, method: "PATCH" });
    }
    async farmGemOnce() {
      const idReward = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
      const patchUrl = `https://www.duolingo.com/2017-06-30/users/${this.sub}/rewards/${idReward}`;
      const patchBody = {
        consumed: true,
        learningLanguage: this.userInfo.learningLanguage,
        fromLanguage: this.userInfo.fromLanguage
      };
      return await this.sendRequest({ url: patchUrl, payload: patchBody, headers: this.defaultHeaders, method: "PATCH" });
    }
    async farmStoryOnce(config = {}) {
      const startTime = getCurrentUnixTimestamp();
      const fromLanguage = this.userInfo.fromLanguage;
      const completeUrl = `https://stories.duolingo.com/api2/stories/en-${fromLanguage}-the-passport/complete`;
      const storyPayload = {
        awardXp: true,
        isFeaturedStoryInPracticeHub: false,
        completedBonusChallenge: true,
        mode: "READ",
        isV2Redo: false,
        isV2Story: false,
        isLegendaryMode: true,
        masterVersion: false,
        maxScore: 0,
        numHintsUsed: 0,
        score: 0,
        startTime,
        fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        hasXpBoost: false,
        // happyHourBonusXp: 449,
        ...config.storyPayload || {}
      };
      return await this.sendRequest({ url: completeUrl, payload: storyPayload, headers: this.defaultHeaders, method: "POST" });
    }
    async farmSessionOnce(config = {}) {
      const startTime = config.startTime || getCurrentUnixTimestamp();
      const endTime = config.endTime || startTime + 60;
      const sessionPayload = {
        challengeTypes: [],
        fromLanguage: this.userInfo.fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        // isFinalLevel: false,
        // isV2: true,
        // juicy: true,
        // smartTipsVersion: 2,
        type: "GLOBAL_PRACTICE",
        ...config.sessionPayload || {}
      };
      const sessionRes = await this.sendRequest({ url: "https://www.duolingo.com/2017-06-30/sessions", payload: sessionPayload, headers: this.defaultHeaders, method: "POST" });
      const sessionData = await sessionRes.json();
      const updateSessionPayload = {
        // ...sessionData,
        id: sessionData.id,
        metadata: sessionData.metadata,
        type: sessionData.type,
        fromLanguage: this.userInfo.fromLanguage,
        learningLanguage: this.userInfo.learningLanguage,
        challenges: [],
        // empty for fast response
        adaptiveChallenges: [],
        // empty for fast response
        sessionExperimentRecord: [],
        experiments_with_treatment_contexts: [],
        adaptiveInterleavedChallenges: [],
        adaptiveChallenges: [],
        sessionStartExperiments: [],
        trackingProperties: [],
        ttsAnnotations: [],
        heartsLeft: 0,
        startTime,
        enableBonusPoints: false,
        endTime,
        failed: false,
        maxInLessonStreak: 9,
        shouldLearnThings: true,
        ...config.updateSessionPayload || {}
      };
      const updateRes = await this.sendRequest({ url: `https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`, payload: updateSessionPayload, headers: this.defaultHeaders, method: "PUT" });
      return updateRes;
    }
  }
  class SettingsManager {
    constructor(shadowRoot2, apiService2 = null) {
      this.shadowRoot = shadowRoot2;
      this.apiService = apiService2;
      this.DEFAULT_SETTINGS = {
        autoOpenUI: false,
        autoStart: false,
        defaultOption: 1,
        // index of option in OPTIONS array (0-based)
        hideUsername: false,
        keepScreenOn: false,
        delayTime: 500,
        retryTime: 1e3,
        autoStopTime: 0,
        darkMode: false,
        compactUI: false,
        showProgress: false,
        fontSize: "medium"
      };
      this.settings = this.loadSettings();
    }
    loadSettings() {
      try {
        const saved = localStorage.getItem("duofarmerSettings");
        if (saved) {
          return { ...this.DEFAULT_SETTINGS, ...JSON.parse(saved) };
        }
        return { ...this.DEFAULT_SETTINGS };
      } catch (error) {
        return { ...this.DEFAULT_SETTINGS };
      }
    }
    saveSettings(settings) {
      this.settings = settings;
      localStorage.setItem("duofarmerSettings", JSON.stringify(settings));
    }
    getSettings() {
      return { ...this.settings };
    }
    loadSettingsToUI() {
      const elements = this.getElements();
      if (elements.autoOpenUI) elements.autoOpenUI.checked = this.settings.autoOpenUI;
      if (elements.autoStart) elements.autoStart.checked = this.settings.autoStart;
      if (elements.defaultOption) elements.defaultOption.value = this.settings.defaultOption.toString();
      if (elements.hideUsername) elements.hideUsername.checked = this.settings.hideUsername;
      if (elements.keepScreenOn) elements.keepScreenOn.checked = this.settings.keepScreenOn;
      if (elements.delayTime) elements.delayTime.value = this.settings.delayTime;
      if (elements.retryTime) elements.retryTime.value = this.settings.retryTime;
      if (elements.autoStopTime) elements.autoStopTime.value = this.settings.autoStopTime;
      if (elements.darkMode) elements.darkMode.checked = this.settings.darkMode;
      if (elements.compactUI) elements.compactUI.checked = this.settings.compactUI;
      if (elements.showProgress) elements.showProgress.checked = this.settings.showProgress;
      if (elements.fontSize) elements.fontSize.value = this.settings.fontSize;
    }
    saveSettingsFromUI() {
      var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
      const elements = this.getElements();
      const settings = {
        autoOpenUI: ((_a = elements.autoOpenUI) == null ? void 0 : _a.checked) || false,
        autoStart: ((_b = elements.autoStart) == null ? void 0 : _b.checked) || false,
        defaultOption: parseInt((_c = elements.defaultOption) == null ? void 0 : _c.value) || 1,
        // index in OPTIONS array
        hideUsername: ((_d = elements.hideUsername) == null ? void 0 : _d.checked) || false,
        keepScreenOn: ((_e = elements.keepScreenOn) == null ? void 0 : _e.checked) || false,
        delayTime: Math.max(100, Math.min(1e4, parseInt((_f = elements.delayTime) == null ? void 0 : _f.value) || 500)),
        retryTime: Math.max(100, Math.min(1e4, parseInt((_g = elements.retryTime) == null ? void 0 : _g.value) || 1e3)),
        autoStopTime: parseInt((_h = elements.autoStopTime) == null ? void 0 : _h.value) || 0,
        darkMode: ((_i = elements.darkMode) == null ? void 0 : _i.checked) || false,
        compactUI: ((_j = elements.compactUI) == null ? void 0 : _j.checked) || false,
        showProgress: ((_k = elements.showProgress) == null ? void 0 : _k.checked) || false,
        fontSize: ((_l = elements.fontSize) == null ? void 0 : _l.value) || "medium"
      };
      this.saveSettings(settings);
      return settings;
    }
    getElements() {
      return {
        autoOpenUI: this.shadowRoot.getElementById("auto-open-ui"),
        autoStart: this.shadowRoot.getElementById("auto-start"),
        defaultOption: this.shadowRoot.getElementById("default-option"),
        hideUsername: this.shadowRoot.getElementById("hide-username"),
        keepScreenOn: this.shadowRoot.getElementById("keep-screen-on"),
        delayTime: this.shadowRoot.getElementById("delay-time"),
        retryTime: this.shadowRoot.getElementById("retry-time"),
        autoStopTime: this.shadowRoot.getElementById("auto-stop-time"),
        darkMode: this.shadowRoot.getElementById("dark-mode"),
        compactUI: this.shadowRoot.getElementById("compact-ui"),
        showProgress: this.shadowRoot.getElementById("show-progress"),
        fontSize: this.shadowRoot.getElementById("font-size"),
        saveSettings: this.shadowRoot.getElementById("save-settings"),
        quickLogout: this.shadowRoot.getElementById("quick-logout"),
        resetTheme: this.shadowRoot.getElementById("reset-theme"),
        getJwtToken: this.shadowRoot.getElementById("get-jwt-token"),
        resetSetting: this.shadowRoot.getElementById("reset-setting"),
        settingsContainer: this.shadowRoot.getElementById("settings-container"),
        setAccountPublic: this.shadowRoot.getElementById("set-account-public"),
        setAccountPrivate: this.shadowRoot.getElementById("set-account-private")
      };
    }
    addEventListeners() {
      const elements = this.getElements();
      elements.saveSettings.addEventListener("click", () => {
        this.saveSettingsFromUI();
        alert("Settings saved successfully, reload the page to apply changes!");
        confirm("Reload now?") && location.reload();
      });
      elements.quickLogout.addEventListener("click", () => {
        if (confirm("Are you sure you want to logout?")) {
          window.location.href = "https://www.duolingo.com/logout";
        }
      });
      elements.resetTheme.addEventListener("click", () => {
      });
      elements.getJwtToken.addEventListener("click", () => {
        const token = getJwtToken();
        if (token) {
          confirm(`Your JWT Token:

${token}

Copy to clipboard?`) && navigator.clipboard.writeText(token);
        }
      });
      elements.resetSetting.addEventListener("click", () => {
        if (confirm("Reset all settings to default? This cannot be undone.")) {
          localStorage.removeItem("duofarmerSettings");
          this.settings = { ...this.DEFAULT_SETTINGS };
          this.loadSettingsToUI();
          alert("All settings reset successfully! Reload to apply changes.");
        }
      });
      elements.setAccountPublic.addEventListener("click", async () => {
        if (confirm("Are you sure you want to set your account to public?")) {
          try {
            await this.apiService.setPrivacyStatus(false);
            alert("Account set to public successfully! Reload the page to see changes.");
          } catch (error) {
            alert("Failed to set account to public: " + error.message);
          }
        }
      });
      elements.setAccountPrivate.addEventListener("click", async () => {
        if (confirm("Are you sure you want to set your account to private?")) {
          try {
            await this.apiService.setPrivacyStatus(true);
            alert("Account set to private successfully! Reload the page to see changes.");
          } catch (error) {
            alert("Failed to set account to private: " + error.message);
          }
        }
      });
    }
    addEventSettings(container) {
      const elements = this.getElements();
      const settingsBtn = this.shadowRoot.getElementById("settings-btn");
      const settingsContainer = elements.settingsContainer;
      const settingsClose = this.shadowRoot.getElementById("settings-close");
      const toggleModal = (modalElement, mainElement) => ({
        show: () => {
          mainElement.style.display = "none";
          modalElement.style.display = "flex";
        },
        hide: () => {
          modalElement.style.display = "none";
          mainElement.style.display = "flex";
        }
      });
      const settingsModal = toggleModal(settingsContainer, container);
      settingsBtn.addEventListener("click", settingsModal.show);
      settingsClose.addEventListener("click", settingsModal.hide);
    }
    loadDefaultFarmingOption(optionsArray) {
      const select = this.shadowRoot.getElementById("select-option");
      const optionIndex = this.settings.defaultOption;
      select.selectedIndex = optionIndex;
    }
    populateDefaultOptionSelect(optionsArray) {
      const select = this.shadowRoot.getElementById("default-option");
      select.innerHTML = "";
      optionsArray.forEach((opt, index) => {
        const option = document.createElement("option");
        option.value = index.toString();
        option.textContent = opt.label;
        if (opt.disabled) option.disabled = true;
        select.appendChild(option);
      });
    }
  }
  let runtimeSettings = {
    delayTime: 500,
    retryTime: 1e3,
    autoStopTime: 0
  };
  let jwt = null;
  let defaultHeaders = null;
  let userInfo = null;
  let sub = null;
  let skillId = null;
  let isRunning = false;
  let shadowRoot = null;
  let apiService = null;
  let settingsManager = null;
  let farmOptions = [];
  let autoStopTimerId = null;
  const getElements = () => {
    return {
      startBtn: shadowRoot.getElementById("start-btn"),
      stopBtn: shadowRoot.getElementById("stop-btn"),
      select: shadowRoot.getElementById("select-option"),
      floatingBtn: shadowRoot.getElementById("floating-btn"),
      container: shadowRoot.getElementById("container"),
      overlay: shadowRoot.getElementById("overlay"),
      notify: shadowRoot.getElementById("notify"),
      username: shadowRoot.getElementById("username"),
      from: shadowRoot.getElementById("from"),
      learn: shadowRoot.getElementById("learn"),
      streak: shadowRoot.getElementById("streak"),
      gem: shadowRoot.getElementById("gem"),
      xp: shadowRoot.getElementById("xp"),
      settingsBtn: shadowRoot.getElementById("settings-btn"),
      settingsContainer: shadowRoot.getElementById("settings-container"),
      settingsClose: shadowRoot.getElementById("settings-close"),
      userInfoDisplay: shadowRoot.getElementById("user-info-display"),
      setAccountPublic: shadowRoot.getElementById("set-account-public"),
      setAccountPrivate: shadowRoot.getElementById("set-account-private")
    };
  };
  const setRunningState = (running) => {
    isRunning = running;
    const { startBtn, stopBtn, select } = getElements();
    if (running) {
      startBtn.hidden = true;
      stopBtn.hidden = false;
      stopBtn.disabled = true;
      stopBtn.className = "disable-btn";
      select.disabled = true;
    } else {
      stopBtn.hidden = true;
      startBtn.hidden = false;
      startBtn.disabled = true;
      startBtn.className = "disable-btn";
      select.disabled = false;
      if (autoStopTimerId) {
        clearTimeout(autoStopTimerId);
        autoStopTimerId = null;
      }
    }
    setTimeout(() => {
      const { startBtn: btn, stopBtn: stop } = getElements();
      btn.className = "";
      btn.disabled = false;
      stop.className = "";
      stop.disabled = false;
    }, 3e3);
  };
  const disableAllControls = (notifyMessage = null) => {
    const { startBtn, stopBtn, select } = getElements();
    startBtn.disabled = true;
    startBtn.className = "disable-btn";
    stopBtn.disabled = true;
    select.disabled = true;
    if (notifyMessage) {
      updateNotify(notifyMessage);
    }
  };
  const initInterface = () => {
    const container = document.createElement("div");
    shadowRoot = container.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    style.textContent = cssText;
    shadowRoot.appendChild(style);
    const content = document.createElement("div");
    content.innerHTML = templateRaw;
    shadowRoot.appendChild(content);
    document.body.appendChild(container);
    const settingsContainer = shadowRoot.getElementById("settings-container");
    if (settingsContainer) {
      settingsContainer.style.display = "none";
    }
    const requiredElements = [
      "start-btn",
      "stop-btn",
      "select-option",
      "floating-btn",
      "container",
      "overlay",
      "notify"
    ];
    for (const id of requiredElements) {
      if (!shadowRoot.getElementById(id)) {
        throw new Error(`Required UI element '${id}' not found in template. Template may be corrupted.`);
      }
    }
  };
  const showElement = (element) => {
    if (element) element.style.display = "flex";
  };
  const hideElement = (element) => {
    if (element) element.style.display = "none";
  };
  const setInterfaceVisible = (visible) => {
    const { container, overlay } = getElements();
    if (visible) {
      showElement(container);
      showElement(overlay);
    } else {
      hideElement(container);
      hideElement(overlay);
    }
  };
  const addEventFloatingBtn = () => {
    const { floatingBtn } = getElements();
    floatingBtn.addEventListener("click", () => {
      if (isRunning) {
        if (confirm("Duofarmer is farming. Do you want to stop and hide UI?")) {
          setRunningState(false);
          setInterfaceVisible(false);
        }
        return;
      }
      toggleInterface();
    });
  };
  const addEventStartBtn = () => {
    const { startBtn, select } = getElements();
    startBtn.addEventListener("click", async () => {
      setRunningState(true);
      if (runtimeSettings.autoStopTime > 0) {
        autoStopTimerId = setTimeout(() => {
          alert(`Auto-stopped by setting (stop after ${runtimeSettings.autoStopTime} minutes).`);
          updateNotify(`Auto-stopped by setting (stop after ${runtimeSettings.autoStopTime} minutes).`);
          setRunningState(false);
        }, runtimeSettings.autoStopTime * 60 * 1e3);
      }
      const selected = select.options[select.selectedIndex];
      const optionData = {
        type: selected.getAttribute("data-type"),
        amount: Number(selected.getAttribute("data-amount")),
        value: selected.value,
        label: selected.textContent,
        config: selected.getAttribute("data-config") ? JSON.parse(selected.getAttribute("data-config")) : {}
      };
      await farmSelectedOption(optionData);
    });
  };
  const addEventStopBtn = () => {
    const { stopBtn } = getElements();
    stopBtn.addEventListener("click", () => {
      setRunningState(false);
    });
  };
  const isInterfaceVisible = () => {
    const { container } = getElements();
    return container.style.display !== "none" && container.style.display !== "";
  };
  const toggleInterface = () => {
    setInterfaceVisible(!isInterfaceVisible());
  };
  const addEventListeners = () => {
    addEventStartBtn();
    addEventStopBtn();
    const { container } = getElements();
    settingsManager.addEventSettings(container);
    settingsManager.addEventListeners();
  };
  const populateOptions = () => {
    const select = shadowRoot.getElementById("select-option");
    select.innerHTML = "";
    farmOptions.forEach((opt) => {
      const option = document.createElement("option");
      option.value = opt.value;
      option.textContent = opt.label;
      option.setAttribute("data-type", opt.type);
      if (opt.amount != null) option.setAttribute("data-amount", String(opt.amount));
      if (opt.config) option.setAttribute("data-config", JSON.stringify(opt.config));
      if (opt.disabled) option.disabled = true;
      select.appendChild(option);
    });
  };
  const updateNotify = (message) => {
    const { notify } = getElements();
    const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
    notify.innerText = `[${now}] ` + message;
    log(`[${now}] ${message}`);
  };
  const updateUserInfo = () => {
    const elements = getElements();
    if (userInfo) {
      elements.username.innerText = userInfo.username;
      elements.from.innerText = userInfo.fromLanguage;
      elements.learn.innerText = userInfo.learningLanguage;
      elements.streak.innerText = userInfo.streak;
      elements.gem.innerText = userInfo.gems;
      elements.xp.innerText = userInfo.totalXp;
      hideElement(userInfo.privacySettings && (userInfo.privacySettings.includes("DISABLE_FRIENDS_QUESTS") || userInfo.privacySettings.includes("DISABLE_LEADERBOARDS")) ? elements.setAccountPrivate : elements.setAccountPublic);
      elements.userInfoDisplay.innerText = JSON.stringify({
        id: userInfo.id,
        username: userInfo.username,
        fromLanguage: userInfo.fromLanguage,
        learningLanguage: userInfo.learningLanguage,
        streak: userInfo.streak,
        gems: userInfo.gems,
        totalXp: userInfo.totalXp,
        creationDate: userInfo.creationDate,
        skillId,
        jwt: "hidden - use get jwt button to view",
        sub,
        privacySettings: userInfo.privacySettings,
        streakData: userInfo.streakData
      }, null, 2);
    }
  };
  const updateFarmResult = (type, farmedAmount) => {
    switch (type) {
      case "gem":
        userInfo = { ...userInfo, gems: userInfo.gems + farmedAmount };
        updateNotify(`You got ${farmedAmount} gem!!!`);
        break;
      case "xp":
        userInfo = { ...userInfo, totalXp: userInfo.totalXp + farmedAmount };
        updateNotify(`You got ${farmedAmount} XP!!!`);
        break;
      case "streak":
        userInfo = { ...userInfo, streak: userInfo.streak + farmedAmount };
        updateNotify(`You got ${farmedAmount} streak! (maybe some xp too, idk)`);
        break;
    }
    updateUserInfo();
  };
  const gemFarmingLoop = async () => {
    const gemFarmed = 30;
    while (isRunning) {
      try {
        await apiService.farmGemOnce(userInfo);
        updateFarmResult("gem", gemFarmed);
        await delay(runtimeSettings.delayTime);
      } catch (error) {
        updateNotify(`Error ${error.status}! Please report in telegram group!`);
        await delay(runtimeSettings.retryTime);
      }
    }
  };
  const xpFarmingLoop = async (value, amount, config = {}) => {
    while (isRunning) {
      try {
        let response;
        if (value === "session") {
          response = await apiService.farmSessionOnce(config);
        } else if (value === "story") {
          response = await apiService.farmStoryOnce(config);
        }
        if (response.status > 400) {
          updateNotify(`Something went wrong! Pls try other farming methods.
If you are using story method, u should try with English course!`);
          await delay(runtimeSettings.retryTime);
          continue;
        }
        const responseData = await response.json();
        const xpFarmed = (responseData == null ? void 0 : responseData.awardedXp) || (responseData == null ? void 0 : responseData.xpGain) || 0;
        updateFarmResult("xp", xpFarmed);
        await delay(runtimeSettings.delayTime);
      } catch (error) {
        updateNotify(`Error ${error.status}! Please report in telegram group!`);
        await delay(runtimeSettings.retryTime);
      }
    }
  };
  const streakFarmingLoop = async () => {
    const hasStreak = !!userInfo.streakData.currentStreak;
    const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : /* @__PURE__ */ new Date();
    const startFarmStreakTimestamp = toTimestamp(startStreakDate);
    let currentTimestamp = hasStreak ? startFarmStreakTimestamp - 86400 : startFarmStreakTimestamp;
    while (isRunning) {
      try {
        const sessionRes = await apiService.farmSessionOnce({ startTime: currentTimestamp, endTime: currentTimestamp + 60 });
        if (sessionRes) {
          currentTimestamp -= 86400;
          updateFarmResult("streak", 1);
          await delay(runtimeSettings.delayTime);
        } else {
          updateNotify("Failed to farm streak session, I'm trying again...");
          await delay(runtimeSettings.retryTime);
          continue;
        }
      } catch (error) {
        updateNotify(`Error in farmStreak: ${(error == null ? void 0 : error.message) || error}`);
        await delay(runtimeSettings.retryTime);
        continue;
      }
    }
  };
  const farmSelectedOption = async (option) => {
    const { type, value, amount, config } = option;
    switch (type) {
      case "gem":
        gemFarmingLoop();
        break;
      case "xp":
        xpFarmingLoop(value, amount, config);
        break;
      case "streak":
        streakFarmingLoop();
        break;
    }
  };
  const loadSavedSettings = (settings) => {
    runtimeSettings = { ...runtimeSettings, ...settings };
    const elements = getElements();
    if (settings.autoOpenUI) {
      setInterfaceVisible(true);
    }
    if (settings.autoStart) {
      setInterfaceVisible(true);
      elements.startBtn.click();
    }
    if (settings.hideUsername) {
      elements.username.classList.add("blur");
    }
    if (settings.keepScreenOn && "wakeLock" in navigator) {
      navigator.wakeLock.request("screen").then((wakeLock) => {
        log("Screen wake lock active");
      });
    }
    if (settings.darkMode) ;
    if (settings.compactUI) ;
    if (settings.showProgress) ;
    if (settings.fontSize) ;
  };
  const initVariables = async () => {
    jwt = getJwtToken();
    if (!jwt) {
      disableAllControls("Please login to Duolingo and reload!");
      return;
    }
    defaultHeaders = formatHeaders(jwt);
    const decodedJwt = decodeJwtToken(jwt);
    sub = decodedJwt.sub;
    userInfo = await ApiService.getUserInfo(sub, defaultHeaders);
    apiService = new ApiService(jwt, defaultHeaders, userInfo, sub);
    settingsManager = new SettingsManager(shadowRoot, apiService);
    skillId = extractSkillId(userInfo.currentCourse || {});
    farmOptions = [
      { type: "separator", label: "⟡ GEM FARMING ⟡", value: "", disabled: true },
      { type: "gem", label: "Gem 30", value: "fixed", amount: 30 },
      { type: "separator", label: "⟡ XP SESSION FARMING ⟡", value: "", disabled: true },
      { type: "separator", label: "(slow, safe, any language)", value: "", disabled: true },
      { type: "xp", label: "XP 10", value: "session", amount: 10, config: {} },
      // { type: 'xp', label: 'XP 13', value: 'session', amount: 13, config: { updateSessionPayload: { enableBonusPoints: true } } },
      { type: "xp", label: "XP 20", value: "session", amount: 20, config: { updateSessionPayload: { hasBoost: true } } },
      // { type: 'xp', label: 'XP 26', value: 'session', amount: 26, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true } } },
      // { type: 'xp', label: 'XP 36', value: 'session', amount: 36, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10 } } },
      { type: "xp", label: "XP 40", value: "session", amount: 40, config: { updateSessionPayload: { hasBoost: true, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 50", value: "session", amount: 50, config: { updateSessionPayload: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10, type: "TARGET_PRACTICE" } } },
      { type: "xp", label: "XP 110", value: "session", amount: 110, config: { sessionPayload: { type: "UNIT_TEST", skillIds: skillId ? [skillId] : [] }, updateSessionPayload: { type: "UNIT_TEST", hasBoost: true, happyHourBonusXp: 10, pathLevelSpecifics: { unitIndex: 0 } } }, disabled: !skillId },
      // {
      // 	type: 'xp', label: 'TEST', value: 'session', amount: 0, config: {
      // 		sessionPayload: { type: 'UNIT_TEST', skillIds: skillId ? [skillId] : [] },
      // 		updateSessionPayload: {
      // 			hasBoost: true,
      // 			happyHourBonusXp: 10,
      // 			pathLevelSpecifics: {
      // 				unitIndex: 0,
      // 			}
      // 		}
      // 	},
      // 	disabled: !skillId
      // },
      { type: "separator", label: "⟡ XP STORY FARMING ⟡", value: "", disabled: true },
      { type: "separator", label: "(fast, unsafe, English only) ", value: "", disabled: true },
      { type: "xp", label: "XP 50", value: "story", amount: 50, config: {} },
      // { type: 'xp', label: 'XP 90 ', value: 'story', amount: 90, config: { storyPayload: { hasXpBoost: true } } },
      { type: "xp", label: "XP 100 ", value: "story", amount: 100, config: { storyPayload: { happyHourBonusXp: 50 } } },
      { type: "xp", label: "XP 200 ", value: "story", amount: 200, config: { storyPayload: { happyHourBonusXp: 150 } } },
      { type: "xp", label: "XP 300 ", value: "story", amount: 300, config: { storyPayload: { happyHourBonusXp: 250 } } },
      { type: "xp", label: "XP 400 ", value: "story", amount: 400, config: { storyPayload: { happyHourBonusXp: 350 } } },
      { type: "xp", label: "XP 499 ", value: "story", amount: 499, config: { storyPayload: { happyHourBonusXp: 449 } } },
      { type: "separator", label: "⟡ STREAK FARMING ⟡", value: "", disabled: true },
      { type: "streak", label: "Streak farm (test)", value: "farm" }
    ];
  };
  const initSettings = () => {
    settingsManager.populateDefaultOptionSelect(farmOptions);
    settingsManager.loadDefaultFarmingOption(farmOptions);
    settingsManager.loadSettingsToUI();
  };
  (async () => {
    try {
      initInterface();
      setInterfaceVisible(false);
      addEventFloatingBtn();
      await initVariables();
      populateOptions();
      initSettings();
      updateUserInfo();
      addEventListeners();
      loadSavedSettings(settingsManager.getSettings());
      updateNotify('Duofarmer ready! For safety, I suggest that you use 2nd accounts.\nLimited or no use of "Story Farming"!');
    } catch (err) {
      logError(err, "Duofarmer init error!");
    }
  })();

})();