您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
DuoFarmer is a tool that helps you farm XP, farm Streak, farm Gems or even repair frozen streak on Duolingo!.
当前为
// ==UserScript== // @name Duolingo DuoFarmer // @namespace https://duo-farmer.vercel.app // @version 1.3.6 // @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>\n<div id="container">\n <div id="header">\n <span class="label">Duofarmer</span>\n <button id="settings-btn">⚙️</button>\n </div>\n <div id="body">\n <table id="table-main" class="table">\n <thead>\n <tr>\n <th>Username</th>\n <th>From</th>\n <th>Learning</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td id="username">duofarmer</td>\n <td id="from">any</td>\n <td id="learn">any</td>\n </tr>\n </tbody>\n </table>\n <table id="table-progress" class="table">\n <thead>\n <tr>\n <th>Streak</th>\n <th>Gem</th>\n <th>XP</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td id="streak">0</td>\n <td id="gem">0</td>\n <td id="xp">0</td>\n </tr>\n </tbody>\n </table>\n <div id="action-row">\n <select id="select-option">\n <!-- <option value="option1">Option 1</option> -->\n <!-- <option value="option2">Option 2</option> -->\n </select>\n <button id="start-btn">Start</button>\n <button id="stop-btn" hidden>Stop</button>\n </div>\n <div id="notify">High ban risk! Use with caution.<br /></div>\n </div>\n <div id="footer">\n <span class="label">u gay 💔</span>\n </div>\n</div>\n<div id="settings-container">\n <div id="settings-menu" class="modal-content">\n <div class="modal-header">\n <span class="label">Settings</span>\n <button id="settings-close" class="modal-close">✕</button>\n </div>\n <div class="modal-body">\n <div class="settings-group">\n <h3>General</h3>\n <div class="setting-item">\n <span>Auto open UI onload</span>\n <input type="checkbox" id="auto-open-ui">\n </div>\n <div class="setting-item">\n <span>Auto start farming onload</span>\n <input type="checkbox" id="auto-start">\n </div>\n <div class="setting-item">\n <span>Default farming option</span>\n <select id="default-option">\n <!-- option auto -->\n </select>\n </div>\n <div class="setting-item">\n <span>Hide username</span>\n <input type="checkbox" id="hide-username">\n </div>\n <div class="setting-item">\n <span>Keep screen on</span>\n <input type="checkbox" id="keep-screen-on">\n </div>\n </div>\n <div class="settings-group disabled">\n <h3>Performance (coming soon)</h3>\n <div class="setting-item">\n <span>Delay time (ms):</span>\n <input type="number" id="delay-time" min="300" max="10000" value="500">\n </div>\n <div class="setting-item">\n <span>Retry time (ms):</span>\n <input type="number" id="retry-time" min="300" max="10000" value="1000">\n </div>\n <div class="setting-item">\n <span>Auto stop after (min) (set 0 for unlimited)</span>\n <input type="number" id="auto-stop-time" min="0" max="60" value="0">\n </div>\n </div>\n <div class="settings-group disabled">\n <h3>Interface (coming soon)</h3>\n <div class="setting-item">\n <span>Dark mode</span>\n <input type="checkbox" id="dark-mode">\n </div>\n <div class="setting-item">\n <span>Compact UI</span>\n <input type="checkbox" id="compact-ui">\n </div>\n <div class="setting-item">\n <span>Show progress bar</span>\n <input type="checkbox" id="show-progress">\n </div>\n <div class="setting-item">\n <span>Font size</span>\n <select id="font-size">\n <option value="small">Small</option>\n <option value="medium">Medium</option>\n <option value="large">Large</option>\n </select>\n </div>\n <div class="setting-item">\n <span>Reset theme</span>\n <button id="reset-theme" class="setting-btn">Reset</button>\n </div>\n </div>\n <div class="settings-group">\n <h3>Advanced</h3>\n <div class="setting-item">\n <span>Get ur JWT token</span>\n <button id="get-jwt-token" class="setting-btn">Get Token</button>\n </div>\n <div class="setting-item">\n <span>Quick logout</span>\n <button id="quick-logout" class="setting-btn">Logout</button>\n </div>\n <div class="setting-item">\n <span>Reset setting</span>\n <button id="reset-setting" class="setting-btn">Reset</button>\n </div>\n </div>\n <div class="settings-group">\n <h3>Others</h3>\n <div class="setting-item">\n <span>Blank page (best performance)</span>\n <a href="https://www.duolingo.com/errors/404.html">Here</a>\n </div>\n <div class="setting-item">\n <span>Duolingo homepage</span>\n <a href="https://www.duolingo.com/">Here</a>\n </div>\n <div class="setting-item">\n <span>Greasyfork</span>\n <a href="https://greasyfork.org/vi/scripts/528621-duofarmer" target="_blank">Here</a>\n </div>\n <div class="setting-item">\n <span>Telegram</span>\n <a href="https://t.me/duofarmer" target="_blank">Here</a>\n </div>\n <div class="setting-item">\n <span>Duofarmer Homepage</span>\n <a href="https://duo-farmer.vercel.app" target="_blank">Here</a>\n </div>\n </div>\n </div>\n <div class="modal-footer">\n <span></span>\n <button id="save-settings" class="save-btn">Save</button>\n </div>\n </div>\n</div>\n<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}"; 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) { if (level.state === "active") { const skillId = ((_a = level.pathLevelMetadata) == null ? void 0 : _a.skillId) || ((_b = level.pathLevelClientData) == null ? void 0 : _b.skillId); if (skillId) return skillId; } } } } 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,currentCourse`; 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 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 patchData = { consumed: true, learningLanguage: this.userInfo.learningLanguage, fromLanguage: this.userInfo.fromLanguage }; return await this.sendRequest({ url: patchUrl, payload: patchData, 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: [ "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: this.userInfo.fromLanguage, isFinalLevel: false, isV2: true, juicy: true, learningLanguage: this.userInfo.learningLanguage, 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, challenges: [], // empty for fast response adaptiveChallenges: [], // empty for fast response sessionExperimentRecord: [], // metadata: {}, 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) { this.shadowRoot = shadowRoot2; 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) { logError("Settings load error:", 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: parseInt((_f = elements.delayTime) == null ? void 0 : _f.value) || 500, retryTime: 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") }; } 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."); } }); } 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); }); } } const DELAY = 500; const ERROR_DELAY = 1e3; let jwt, defaultHeaders, userInfo, sub, apiService; let isRunning = false; let shadowRoot = null; let settingsManager = null; let farmOptions = []; 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") }; }; 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; } 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); 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 = () => { addEventFloatingBtn(); 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 { username, from, learn, streak, gem, xp } = getElements(); if (userInfo) { 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 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(DELAY); } catch (error) { updateNotify(`Error ${error.status}! Please report in telegram group!`); await delay(ERROR_DELAY); } } }; 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(ERROR_DELAY); 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(DELAY); } catch (error) { updateNotify(`Error ${error.status}! Please report in telegram group!`); await delay(ERROR_DELAY); } } }; 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(DELAY); } else { updateNotify("Failed to farm streak session, I'm trying again..."); await delay(ERROR_DELAY); continue; } } catch (error) { updateNotify(`Error in farmStreak: ${(error == null ? void 0 : error.message) || error}`); await delay(ERROR_DELAY); 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) => { 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"); }).catch((err) => { logError("Wake lock failed:", err); }); } if (settings.delayTime) ; if (settings.retryTime) ; if (settings.autoStopTime) ; 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); const 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: { 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" } ]; apiService = new ApiService(jwt, defaultHeaders, userInfo, sub); populateOptions(); settingsManager = new SettingsManager(shadowRoot); settingsManager.getSettings(); settingsManager.populateDefaultOptionSelect(farmOptions); settingsManager.loadDefaultFarmingOption(farmOptions); settingsManager.loadSettingsToUI(); }; (async () => { try { initInterface(); setInterfaceVisible(false); await initVariables(); updateUserInfo(); addEventListeners(); loadSavedSettings(settingsManager.getSettings()); } catch (err) { logError(err, "init main.js"); } })(); })();