// ==UserScript==
// @name DuoFarmer
// @namespace https://duo-farmer.vercel.app
// @version 1.3.5
// @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" target="_blank">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>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
};
};
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`;
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 payload = {
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: "en",
hasXpBoost: false,
// happyHourBonusXp: 449,
...config
};
return await this.sendRequest({ url: completeUrl, payload, 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"
};
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,
heartsLeft: 0,
startTime,
enableBonusPoints: false,
endTime,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
...config
};
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;
const OPTIONS = [
{ 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 but safe)", value: "", disabled: true },
{ type: "xp", label: "XP 10", value: "session", amount: 10, config: {} },
{ type: "xp", label: "XP 13", value: "session", amount: 13, config: { enableBonusPoints: true } },
{ type: "xp", label: "XP 20", value: "session", amount: 20, config: { hasBoost: true } },
{ type: "xp", label: "XP 26", value: "session", amount: 26, config: { enableBonusPoints: true, hasBoost: true } },
{ type: "xp", label: "XP 36", value: "session", amount: 36, config: { enableBonusPoints: true, hasBoost: true, happyHourBonusXp: 10 } },
{ 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: 0, config: {} },
{ type: "xp", label: "XP 90 ", value: "story", amount: 0, config: { hasXpBoost: true } },
{ type: "xp", label: "XP 100 ", value: "story", amount: 100, config: { happyHourBonusXp: 50 } },
{ type: "xp", label: "XP 200 ", value: "story", amount: 200, config: { happyHourBonusXp: 150 } },
{ type: "xp", label: "XP 300 ", value: "story", amount: 300, config: { happyHourBonusXp: 250 } },
{ type: "xp", label: "XP 400 ", value: "story", amount: 400, config: { happyHourBonusXp: 350 } },
{ type: "xp", label: "XP 499 ", value: "story", amount: 499, config: { happyHourBonusXp: 449 } },
{ type: "separator", label: "⟡ STREAK FARMING ⟡", value: "", disabled: true },
{ type: "streak", label: "Streak farm (test)", value: "farm" }
];
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 = "";
OPTIONS.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 record screen and 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, make sure you are on English course (learning language == en)!`);
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 record screen and 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);
apiService = new ApiService(jwt, defaultHeaders, userInfo, sub);
populateOptions();
settingsManager = new SettingsManager(shadowRoot);
settingsManager.getSettings();
settingsManager.populateDefaultOptionSelect(OPTIONS);
settingsManager.loadDefaultFarmingOption(OPTIONS);
settingsManager.loadSettingsToUI();
};
(async () => {
try {
initInterface();
setInterfaceVisible(false);
await initVariables();
updateUserInfo();
addEventListeners();
loadSavedSettings(settingsManager.getSettings());
} catch (err) {
logError(err, "init main.js");
}
})();
})();