// ==UserScript==
// @name DuoHacker
// @name:zh-CN DuoHacker — 免费 Duolingo 农场工具
// @name:ja DuoHacker — 無料の Duolingo ファーミングツール
// @name:es DuoHacker — Herramienta gratuita para farmear en Duolingo
// @name:ru DuoHacker — Бесплатный инструмент для фарминга Duolingo
// @name:pt-BR DuoHacker — Melhor ferramenta gratuita para farmar no Duolingo
// @description Best free-to-use Duolingo farming tool!
// @description:zh-CN 免费且易用的 Duolingo 农场工具!
// @description:ja 無料で使える Duolingo のファーミングツールです!
// @description:es ¡La mejor herramienta gratuita para farmear en Duolingo!
// @description:ru Лучший бесплатный инструмент для фарминга в Duolingo!
// @description:pt-BR A melhor ferramenta gratuita para farmar no Duolingo!
// @namespace https://irylisvps.vercel.app
// @version 1.0.1
// @author DuoHacker Community
// @match https://*.duolingo.com/*
// @icon https://github.com/pillowslua/images/blob/main/Hacklingo.png?raw=true
// @grant none
// @license MIT
// ==/UserScript==
const VERSION = "1.0";
const DELAY = 300;
const MAX_THREADS = 5;
var jwt, defaultHeaders, userInfo, sub;
let isRunning = false;
let currentTheme = localStorage.getItem('duofarmer_theme') || 'dark';
let hasJoined = localStorage.getItem('duofarmer_joined') === 'true';
let activeThreads = [];
let totalEarned = { xp: 0, gems: 0, streak: 0 };
let farmingStats = { sessions: 0, errors: 0, startTime: null };
const initInterface = () => {
const containerHTML = `
<div id="_backdrop"></div>
<div id="_container" class="theme-${currentTheme}">
<!-- Header with TwiskGPT inspired design -->
<div id="_header">
<div class="_header_content">
<div class="_logo_section">
<div class="_logo">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.9-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>
</div>
<div class="_title_section">
<h1>DuoHacker PRO</h1>
<span class="_subtitle">BEST Duolingo Farming Tool</span>
</div>
</div>
<div class="_header_actions">
<button id="_minimize_btn" class="_header_btn" title="Minimize">-</button>
<button id="_settings_btn" class="_header_btn" title="Settings">⚙</button>
<button id="_close_btn" class="_header_btn _close" title="Close">x</button>
</div>
</div>
</div>
<!-- Join Discord section -->
<div id="_join_section" class="_section ${hasJoined ? '_collapsed' : ''}">
<div class="_section_header" onclick="toggleSection('_join_section')">
<div class="_section_title">
<span class="_section_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="12" x="2" y="6" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12c.5-1 3-3 6-3s5.5 2 6 3"/><path d="M6 12c.5 1 3 3 6 3s5.5-2 6-3"/></svg>
</span>
<span>Join Our Community</span>
</div>
<div class="_section_status">
<span id="_join_status" class="${hasJoined ? '_status_success' : '_status_error'}">
${hasJoined ? '✓ Joined' : '⚠ Required'}
</span>
<span class="_chevron ${hasJoined ? '_rotated' : ''}">▼</span>
</div>
</div>
<div class="_section_content" ${hasJoined ? 'style="display:none"' : ''}>
<div class="_input_container">
<button id="_join_btn" class="_primary_btn">
<span class="_btn_content">
<span class="_btn_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg>
</span>
<span class="_btn_text">Join Discord</span>
</span>
</button>
</div>
<div class="_help_text">
Join our Discord server for updates and support!
</div>
</div>
</div>
<!-- Main content (hidden until joined) -->
<div id="_main_content" ${hasJoined ? '' : 'style="display:none"' }>
<!-- User info section -->
<div id="_user_section" class="_section">
<div class="_section_header">
<div class="_section_title">
<span class="_section_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span>Account Overview</span>
</div>
<button id="_refresh_btn" class="_icon_btn" title="Refresh data">↻</button>
</div>
<div class="_section_content">
<div class="_stats_grid">
<div class="_stat_card">
<div class="_stat_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="_stat_content">
<div class="_stat_label">Username</div>
<div class="_stat_value" id="_username">Loading...</div>
</div>
</div>
<div class="_stat_card">
<div class="_stat_icon">🌎</div>
<div class="_stat_content">
<div class="_stat_label">Languages</div>
<div class="_stat_value">
<span id="_from_lang">--</span> -> <span id="_learning_lang">--</span>
</div>
</div>
</div>
<div class="_stat_card">
<div class="_stat_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/></svg>
</div>
<div class="_stat_content">
<div class="_stat_label">Streak</div>
<div class="_stat_value" id="_current_streak">0</div>
</div>
</div>
<div class="_stat_card">
<div class="_stat_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>
</div>
<div class="_stat_content">
<div class="_stat_label">Gems</div>
<div class="_stat_value" id="_current_gems">0</div>
</div>
</div>
<div class="_stat_card">
<div class="_stat_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" x2="12" y1="2" y2="22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<div class="_stat_content">
<div class="_stat_label">Total XP</div>
<div class="_stat_value" id="_current_xp">0</div>
</div>
</div>
</div>
</div>
</div>
<!-- Farming control section -->
<div id="_farming_section" class="_section">
<div class="_section_header">
<div class="_section_title">
<span class="_section_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 22 16 17"/><path d="M14 16.5V5.5"/><path d="M15 18.5c-1.4 1-3.1 1.5-4.7 1-1.3.4-2.6-.2-3.5-1.1-.8-.8-1.3-2-1-3.3.6-2.5 4-3.8 3.8-6.1-.3-1.6-1.4-2.8-2.9-3.3"/><path d="M20.97 21.5c.2-1.1-.3-2.5-1-3.5-.7-1-2-1.6-3.5-1.1-2.7.5-3.1 2.2-3.7 4.2-.3 1.3.2 2.6 1.1 3.5.9.9 3.7 1.3 5.1.4"/><path d="M3 5.5 5 17"/><path d="M18.5 16H20c.3 0 .5-.2.5-.5V6.3c0-.3-.2-.5-.5-.5h-1.2c-.3 0-.5.2-.5.5v.3c0 .3.2.5.5.5h.5v8"/></svg>
</span>
<span>Farming Controls</span>
</div>
<div class="_thread_indicator">
<span class="_thread_count">Threads: <strong id="_active_threads">0</strong>/${MAX_THREADS}</span>
</div>
</div>
<div class="_section_content">
<div class="_farming_controls">
<div class="_control_row">
<select id="_farming_mode" class="_modern_select">
<option value="xp_multi">⚡ Multi-threaded XP Farming</option>
<option value="gem_multi">💎 Multi-threaded Gem Farming</option>
<option value="streak_repair">🔧 Streak Repair (Single)</option>
<option value="streak_farm">🔥 Streak Farming (Single)</option>
<option value="mixed_farming">🎯 Mixed Farming (All types)</option>
</select>
<div class="_thread_control">
<label>Threads:</label>
<input type="range" id="_thread_slider" min="1" max="${MAX_THREADS}" value="3" class="_slider">
<span id="_thread_display">3</span>
</div>
</div>
<div class="_control_row">
<button id="_start_farming" class="_primary_btn _large">
<span class="_btn_content">
<span class="_btn_icon">▶</span>
<span class="_btn_text">Start Farming</span>
</span>
</button>
<button id="_stop_farming" class="_danger_btn _large" style="display:none">
<span class="_btn_content">
<span class="_btn_icon">⏹</span>
<span class="_btn_text">Stop Farming</span>
</span>
</button>
<button id="_pause_farming" class="_secondary_btn" style="display:none">
<span class="_btn_content">
<span class="_btn_icon">⏨</span>
<span class="_btn_text">Pause</span>
</span>
</button>
</div>
</div>
</div>
</div>
<!-- Statistics section -->
<div id="_stats_section" class="_section">
<div class="_section_header">
<div class="_section_title">
<span class="_section_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18.7 8l-5.1 5.2-2.8-2.9L7 14.1"/></svg>
</span>
<span>Live Statistics</span>
</div>
<button id="_reset_stats" class="_icon_btn" title="Reset statistics">↻</button>
</div>
<div class="_section_content">
<div class="_stats_grid _live_stats">
<div class="_stat_card _earned">
<div class="_stat_icon">⚡</div>
<div class="_stat_content">
<div class="_stat_label">XP Earned</div>
<div class="_stat_value" id="_earned_xp">0</div>
</div>
</div>
<div class="_stat_card _earned">
<div class="_stat_icon">💎</div>
<div class="_stat_content">
<div class="_stat_label">Gems Earned</div>
<div class="_stat_value" id="_earned_gems">0</div>
</div>
</div>
<div class="_stat_card _earned">
<div class="_stat_icon">🔥</div>
<div class="_stat_content">
<div class="_stat_label">Streak Gained</div>
<div class="_stat_value" id="_earned_streak">0</div>
</div>
</div>
<div class="_stat_card">
<div class="_stat_icon">🎯</div>
<div class="_stat_content">
<div class="_stat_label">Success Rate</div>
<div class="_stat_value" id="_success_rate">100%</div>
</div>
</div>
</div>
<div class="_progress_section">
<div class="_progress_header">
<span>Farming Progress</span>
<span id="_farming_time">00:00:00</span>
</div>
<div class="_thread_progress">
<div id="_thread_bars"></div>
</div>
</div>
</div>
</div>
<!-- Console output -->
<div id="_console_section" class="_section">
<div class="_section_header">
<div class="_section_title">
<span class="_section_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
</span>
<span>Console Output</span>
</div>
<div class="_console_controls">
<button id="_clear_console" class="_icon_btn" title="Clear console">🗑</button>
<button id="_auto_scroll" class="_icon_btn _active" title="Auto scroll">📜</button>
</div>
</div>
<div class="_section_content">
<div id="_console_output" class="_console">
<div class="_console_line _info">
<span class="_timestamp">[${new Date().toLocaleTimeString()}]</span>
<span class="_message">DuoNexus v${VERSION} initialized</span>
</div>
<div class="_console_line _info">
<span class="_timestamp">[${new Date().toLocaleTimeString()}]</span>
<span class="_message">Multi-threading engine ready</span>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="_settings_modal" class="_modal" style="display:none">
<div class="_modal_overlay"></div>
<div class="_modal_container">
<div class="_modal_header">
<h2>Settings & Preferences</h2>
<button id="_close_modal" class="_icon_btn">x</button>
</div>
<div class="_modal_content">
<div class="_setting_group">
<h3>🎨 Appearance</h3>
<div class="_theme_selector">
<button class="_theme_option ${currentTheme === 'dark' ? '_active' : ''}" data-theme="dark">
<span class="_theme_preview _dark"></span>
<span>Dark Mode</span>
</button>
<button class="_theme_option ${currentTheme === 'light' ? '_active' : ''}" data-theme="light">
<span class="_theme_preview _light"></span>
<span>Light Mode</span>
</button>
</div>
</div>
<div class="_setting_group">
<h3>⚡ Performance</h3>
<div class="_setting_item">
<label>Request Delay (ms)</label>
<input type="range" id="_delay_slider" min="100" max="1000" value="${DELAY}" class="_slider">
<span id="_delay_value">${DELAY}ms</span>
</div>
<div class="_setting_item">
<label>Auto-pause on errors</label>
<input type="checkbox" id="_auto_pause" class="_checkbox" checked>
</div>
</div>
<div class="_setting_group">
<h3>🔗 Quick Actions</h3>
<div class="_action_buttons">
<a href="https://www.duolingo.com/errors/404.html" target="_blank" class="_action_btn">
📄 Blank Page
</a>
<button id="_export_stats" class="_action_btn">
📊 Export Stats
</button>
<button id="_backup_settings" class="_action_btn">
💾 Backup Settings
</button>
</div>
</div>
<div class="_setting_group">
<h3>ℹ About</h3>
<div class="_about_section">
<p><strong>DuoHacker v${VERSION}</strong></p>
<p>Join our community for FREE SUPPORT 24/7 and PRO version</p>
<p>Developer: <strong>tw1sk</strong></p>
<p>Free-to-use best farming tool!</p>
<div class="_social_links">
<a href="https://discord.gg/Gvmd7deFtS" target="_blank" class="_social_btn">💬 Discord</a>
<a href="https://irylisvps.vercel.app/" target="_blank" class="_social_btn">📦 Website</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div id="_footer">
<div class="_footer_content">
<span>© 2025 DuoHacker by <strong>tw1sk</strong></span>
<span class="_version">v${VERSION}</span>
</div>
</div>
</div>
<!-- DuoHX GPT -->
<div id="_floating_container">
<div id="_floating_btn" class="theme-${currentTheme}" title="Open DuoNexus">
<div class="_btn_ripple"></div>
<div class="_btn_icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.9-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>
</div>
</div>
</div>
`;
const style = document.createElement("style");
style.innerHTML = `
/* CSS Variables for theming */
:root {
--animation-duration: 0.2s;
--animation-ease: cubic-bezier(0.4, 0, 0.2, 1);
--border-radius-sm: 6px;
--border-radius-md: 12px;
--border-radius-lg: 20px;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.2);
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-success: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--gradient-danger: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
/* Dark Theme */
.theme-dark {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-quaternary: #30363d;
--text-primary: #f0f6fc;
--text-secondary: #8b949e;
--text-tertiary: #6e7681;
--border-primary: #30363d;
--border-secondary: #21262d;
--accent-primary: #58a6ff;
--accent-secondary: #1f6feb;
--success-color: #3fb950;
--error-color: #f85149;
--warning-color: #d29922;
}
/* Light Theme */
.theme-light {
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--bg-tertiary: #eaeef2;
--bg-quaternary: #d0d7de;
--text-primary: #24292f;
--text-secondary: #656d76;
--text-tertiary: #8c959f;
--border-primary: #d0d7de;
--border-secondary: #eaeef2;
--accent-primary: #0969da;
--accent-secondary: #0550ae;
--success-color: #1a7f37;
--error-color: #cf222e;
--warning-color: #9a6700;
}
/* Reset and base styles */
* {
box-sizing: border-box;
}
/* Container */
#_container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 95vw;
max-width: 1200px;
max-height: 90vh;
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
color: var(--text-primary);
z-index: 9999;
display: flex;
flex-direction: column;
overflow: hidden;
animation: containerSlideIn var(--animation-duration) var(--animation-ease);
}
@keyframes containerSlideIn {
from {
opacity: 0;
transform: translate(-50%, -60%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
/* Backdrop */
#_backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
z-index: 9998;
animation: backdropFadeIn var(--animation-duration) var(--animation-ease);
}
@keyframes backdropFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Header */
#_header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
padding: 16px 20px;
}
._header_content {
display: flex;
justify-content: space-between;
align-items: center;
}
._logo_section {
display: flex;
align-items: center;
gap: 12px;
}
._logo {
width: 40px;
height: 40px;
background: var(--gradient-primary);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
animation: logoSpin 2s ease-in-out infinite;
}
@keyframes logoSpin {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(180deg); }
}
._title_section h1 {
margin: 0;
font-size: 20px;
font-weight: 600;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
._subtitle {
font-size: 12px;
color: var(--text-secondary);
margin: 0;
}
._header_actions {
display: flex;
gap: 8px;
}
._header_btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: var(--border-radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all var(--animation-duration) var(--animation-ease);
}
._header_btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
transform: scale(1.1);
}
._header_btn._close:hover {
background: var(--error-color);
color: white;
}
/* Sections */
._section {
border-bottom: 1px solid var(--border-secondary);
}
._section_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
cursor: pointer;
background: var(--bg-secondary);
transition: background var(--animation-duration) var(--animation-ease);
}
._section_header:hover {
background: var(--bg-tertiary);
}
._section_title {
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
font-size: 16px;
}
._section_icon {
font-size: 20px;
display: flex;
align-items: center;
}
._section_icon svg {
width: 20px;
height: 20px;
}
._section_status {
display: flex;
align-items: center;
gap: 8px;
}
._status_success {
color: var(--success-color);
font-weight: 600;
}
._status_error {
color: var(--error-color);
font-weight: 600;
}
._chevron {
transition: transform var(--animation-duration) var(--animation-ease);
color: var(--text-tertiary);
}
._chevron._rotated {
transform: rotate(180deg);
}
._section._collapsed ._section_content {
display: none;
}
._section_content {
padding: 20px;
background: var(--bg-primary);
}
/* Input elements */
._input_container {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
._modern_input, ._modern_select {
flex: 1;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-md);
color: var(--text-primary);
font-size: 14px;
transition: all var(--animation-duration) var(--animation-ease);
font-family: inherit;
}
._modern_input:focus, ._modern_select:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
}
._modern_select {
cursor: pointer;
}
/* Buttons */
._primary_btn, ._secondary_btn, ._danger_btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
border: none;
border-radius: var(--border-radius-md);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all var(--animation-duration) var(--animation-ease);
text-decoration: none;
white-space: nowrap;
}
._primary_btn {
background: var(--accent-primary);
color: white;
}
._primary_btn:hover {
background: var(--accent-secondary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
._secondary_btn {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-primary);
}
._secondary_btn:hover {
background: var(--bg-quaternary);
transform: translateY(-1px);
}
._danger_btn {
background: var(--error-color);
color: white;
}
._danger_btn:hover {
background: #d73a49;
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
._large {
padding: 14px 28px;
font-size: 16px;
font-weight: 600;
}
._btn_content {
display: flex;
align-items: center;
gap: 8px;
}
._btn_icon {
font-size: 16px;
}
._icon_btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: var(--border-radius-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all var(--animation-duration) var(--animation-ease);
}
._icon_btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
transform: scale(1.1);
}
._icon_btn._active {
background: var(--accent-primary);
color: white;
}
/* Stats grid */
._stats_grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
._stat_card {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-md);
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
transition: all var(--animation-duration) var(--animation-ease);
}
._stat_card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
border-color: var(--accent-primary);
}
._stat_card._earned {
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
border-color: var(--success-color);
}
._stat_icon {
width: 40px;
height: 40px;
background: var(--gradient-primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
._stat_icon svg {
width: 24px;
height: 24px;
}
._stat_content {
flex: 1;
}
._stat_label {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 4px;
font-weight: 500;
}
._stat_value {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
/* Farming controls */
._farming_controls {
display: flex;
flex-direction: column;
gap: 16px;
}
._control_row {
display: flex;
align-items: center;
gap: 16px;
}
._thread_control {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
._thread_indicator {
background: var(--bg-tertiary);
padding: 6px 12px;
border-radius: var(--border-radius-sm);
font-size: 12px;
color: var(--text-secondary);
}
._slider {
width: 80px;
height: 4px;
border-radius: 2px;
background: var(--bg-tertiary);
outline: none;
cursor: pointer;
}
._slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
box-shadow: var(--shadow-sm);
}
/* Progress bars */
._progress_section {
margin-top: 16px;
}
._progress_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
}
._thread_progress {
background: var(--bg-secondary);
border-radius: var(--border-radius-sm);
padding: 12px;
}
._thread_bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 12px;
}
._thread_bar:last-child {
margin-bottom: 0;
}
._thread_id {
width: 60px;
color: var(--text-secondary);
}
._thread_progress_bar {
flex: 1;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
}
._thread_progress_fill {
height: 100%;
background: var(--gradient-success);
transition: width 0.5s ease;
border-radius: 3px;
}
._thread_status {
width: 80px;
text-align: right;
color: var(--text-tertiary);
font-size: 11px;
}
/* Console */
._console {
height: 200px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-md);
padding: 12px;
overflow-y: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
}
._console_line {
display: flex;
gap: 8px;
margin-bottom: 2px;
align-items: flex-start;
}
._timestamp {
color: var(--text-tertiary);
font-weight: 500;
flex-shrink: 0;
}
._message {
color: var(--text-primary);
}
._console_line._success ._message {
color: var(--success-color);
}
._console_line._error ._message {
color: var(--error-color);
}
._console_line._warning ._message {
color: var(--warning-color);
}
._console_line._info ._message {
color: var(--accent-primary);
}
._console_controls {
display: flex;
gap: 4px;
}
/* Settings Modal */
._modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
._modal_overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
}
._modal_container {
position: relative;
width: 90%;
max-width: 600px;
max-height: 80vh;
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
animation: modalSlideIn var(--animation-duration) var(--animation-ease);
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
._modal_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
}
._modal_header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
._modal_content {
padding: 20px;
overflow-y: auto;
max-height: calc(80vh - 80px);
}
._setting_group {
margin-bottom: 24px;
}
._setting_group:last-child {
margin-bottom: 0;
}
._setting_group h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
._theme_selector {
display: flex;
gap: 12px;
}
._theme_option {
flex: 1;
padding: 16px;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: var(--border-radius-md);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
transition: all var(--animation-duration) var(--animation-ease);
color: var(--text-primary);
}
._theme_option:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
}
._theme_option._active {
border-color: var(--accent-primary);
background: var(--accent-primary);
color: white;
}
._theme_preview {
width: 32px;
height: 20px;
border-radius: 4px;
position: relative;
}
._theme_preview._dark {
background: linear-gradient(to bottom, #0d1117 0%, #161b22 50%, #21262d 100%);
}
._theme_preview._light {
background: linear-gradient(to bottom, #ffffff 0%, #f6f8fa 50%, #eaeef2 100%);
}
._setting_item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border-secondary);
}
._setting_item:last-child {
border-bottom: none;
}
._setting_item label {
font-weight: 500;
color: var(--text-primary);
}
._checkbox {
width: 18px;
height: 18px;
accent-color: var(--accent-primary);
}
._action_buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
._action_btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--border-radius-md);
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
transition: all var(--animation-duration) var(--animation-ease);
cursor: pointer;
}
._action_btn:hover {
background: var(--bg-tertiary);
border-color: var(--accent-primary);
}
._about_section {
padding: 16px;
background: var(--bg-secondary);
border-radius: var(--border-radius-md);
font-size: 14px;
line-height: 1.6;
}
._about_section p {
margin: 8px 0;
}
._social_links {
display: flex;
gap: 8px;
margin-top: 12px;
}
._social_btn {
padding: 8px 12px;
background: var(--accent-primary);
color: white;
text-decoration: none;
border-radius: var(--border-radius-sm);
font-size: 12px;
font-weight: 500;
transition: all var(--animation-duration) var(--animation-ease);
}
._social_btn:hover {
background: var(--accent-secondary);
transform: translateY(-2px);
}
/* Footer */
#_footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-primary);
padding: 16px 20px;
}
._footer_content {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
}
._version {
background: var(--bg-tertiary);
padding: 4px 8px;
border-radius: var(--border-radius-sm);
font-weight: 600;
}
/* Floating Button - Claude.ai inspired */
#_floating_container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 10001;
}
#_floating_btn {
position: relative;
width: 56px;
height: 56px;
background: var(--accent-primary);
border-radius: 50%;
box-shadow: var(--shadow-lg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--animation-duration) var(--animation-ease);
overflow: hidden;
}
#_floating_btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(88, 166, 255, 0.4);
}
#_floating_btn:active {
transform: scale(0.95);
}
._btn_ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s linear;
background-color: rgba(255, 255, 255, 0.7);
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
._btn_icon {
font-size: 24px;
z-index: 1;
transition: transform var(--animation-duration) var(--animation-ease);
}
#_floating_btn:hover ._btn_icon {
transform: rotate(180deg);
}
/* Responsive Design */
@media (max-width: 768px) {
#_container {
width: 98vw;
max-height: 95vh;
}
._stats_grid {
grid-template-columns: 1fr 1fr;
}
._control_row {
flex-direction: column;
align-items: stretch;
}
._thread_control {
justify-content: center;
}
._modal_container {
width: 95%;
margin: 20px;
}
}
@media (max-width: 480px) {
._stats_grid {
grid-template-columns: 1fr;
}
#_floating_btn {
width: 48px;
height: 48px;
bottom: 16px;
right: 16px;
}
._btn_icon {
font-size: 20px;
}
}
/* Utility Classes */
.hidden {
display: none !important;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--border-primary);
border-radius: 4px;
transition: background var(--animation-duration) var(--animation-ease);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Help text */
._help_text {
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.4;
}
._help_text strong {
color: var(--accent-primary);
font-weight: 600;
}
/* Loading animation */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
._loading {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
`;
document.head.appendChild(style);
const container = document.createElement("div");
container.innerHTML = containerHTML;
document.body.appendChild(container);
};
// Multi-threading worker class
class FarmingWorker {
constructor(id, type, delay = DELAY) {
this.id = id;
this.type = type;
this.delay = delay;
this.isActive = false;
this.stats = { requests: 0, successes: 0, errors: 0 };
this.progressElement = null;
}
async start() {
this.isActive = true;
this.createProgressBar();
logToConsole(`Thread ${this.id} started (${this.type})`, 'success');
while (this.isActive && isRunning) {
try {
await this.doWork();
this.stats.requests++;
this.stats.successes++;
this.updateProgress();
await delay(this.delay + Math.random() * 200); // Add some randomization
} catch (error) {
this.stats.errors++;
logToConsole(`Thread ${this.id} error: ${error.message}`, 'error');
await delay(this.delay * 2); // Back off on error
}
}
logToConsole(`Thread ${this.id} stopped`, 'info');
}
async doWork() {
switch (this.type) {
case 'xp':
return await this.farmXP();
case 'gem':
return await this.farmGem();
default:
throw new Error(`Unknown work type: ${this.type}`);
}
}
async farmXP() {
const response = await farmXpOnce();
if (response.ok) {
const data = await response.json();
const earned = data?.awardedXp || 0;
totalEarned.xp += earned;
updateEarnedStats();
logToConsole(`Thread ${this.id} earned ${earned} XP`, 'success');
return earned;
}
throw new Error(`XP farming failed: ${response.status}`);
}
async farmGem() {
const response = await farmGemOnce();
if (response.ok) {
totalEarned.gems += 30;
updateEarnedStats();
logToConsole(`Thread ${this.id} earned 30 gems`, 'success');
return 30;
}
throw new Error(`Gem farming failed: ${response.status}`);
}
stop() {
this.isActive = false;
if (this.progressElement) {
this.progressElement.remove();
}
}
createProgressBar() {
const container = document.getElementById('_thread_bars');
if (!container) return;
this.progressElement = document.createElement('div');
this.progressElement.className = '_thread_bar';
this.progressElement.innerHTML = `
<div class="_thread_id">Thread ${this.id}</div>
<div class="_thread_progress_bar">
<div class="_thread_progress_fill" style="width: 0%"></div>
</div>
<div class="_thread_status">Starting...</div>
`;
container.appendChild(this.progressElement);
}
updateProgress() {
if (!this.progressElement) return;
const successRate = this.stats.requests > 0 ?
(this.stats.successes / this.stats.requests * 100) : 100;
const fillElement = this.progressElement.querySelector('._thread_progress_fill');
const statusElement = this.progressElement.querySelector('._thread_status');
fillElement.style.width = `${Math.min(successRate, 100)}%`;
statusElement.textContent = `${this.stats.successes}/${this.stats.requests}`;
// Update fill color based on success rate
if (successRate >= 90) {
fillElement.style.background = 'var(--gradient-success)';
} else if (successRate >= 70) {
fillElement.style.background = 'var(--warning-color)';
} else {
fillElement.style.background = 'var(--error-color)';
}
}
}
// Utility functions
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const logToConsole = (message, type = 'info') => {
const console = document.getElementById('_console_output');
if (!console) return;
const timestamp = new Date().toLocaleTimeString();
const line = document.createElement('div');
line.className = `_console_line _${type}`;
line.innerHTML = `
<span class="_timestamp">[${timestamp}]</span>
<span class="_message">${message}</span>
`;
console.appendChild(line);
// Auto scroll if enabled
const autoScroll = document.getElementById('_auto_scroll');
if (autoScroll && autoScroll.classList.contains('_active')) {
console.scrollTop = console.scrollHeight;
}
// Keep only last 100 lines
while (console.children.length > 100) {
console.removeChild(console.firstChild);
}
};
const updateEarnedStats = () => {
const elements = {
xp: document.getElementById('_earned_xp'),
gems: document.getElementById('_earned_gems'),
streak: document.getElementById('_earned_streak')
};
if (elements.xp) elements.xp.textContent = totalEarned.xp.toLocaleString();
if (elements.gems) elements.gems.textContent = totalEarned.gems.toLocaleString();
if (elements.streak) elements.streak.textContent = totalEarned.streak;
// Update success rate
const successRate = farmingStats.sessions > 0 ?
((farmingStats.sessions - farmingStats.errors) / farmingStats.sessions * 100) : 100;
const successElement = document.getElementById('_success_rate');
if (successElement) {
successElement.textContent = `${successRate.toFixed(1)}%`;
}
// Update user info if available
if (userInfo) {
userInfo.totalXp += totalEarned.xp;
userInfo.gems += totalEarned.gems;
userInfo.streak += totalEarned.streak;
updateUserInfo();
}
};
const updateFarmingTime = () => {
if (!farmingStats.startTime) return;
const elapsed = Date.now() - farmingStats.startTime;
const hours = Math.floor(elapsed / 3600000);
const minutes = Math.floor((elapsed % 3600000) / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
const timeElement = document.getElementById('_farming_time');
if (timeElement) {
timeElement.textContent = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
};
// Interface management
const setInterfaceVisible = (visible) => {
const container = document.getElementById("_container");
const backdrop = document.getElementById("_backdrop");
if (container && backdrop) {
container.style.display = visible ? "flex" : "none";
backdrop.style.display = visible ? "block" : "none";
}
};
const isInterfaceVisible = () => {
const container = document.getElementById("_container");
return container && container.style.display !== "none";
};
const toggleInterface = () => {
setInterfaceVisible(!isInterfaceVisible());
};
const toggleSection = (sectionId) => {
const section = document.getElementById(sectionId);
if (section) {
section.classList.toggle('_collapsed');
const chevron = section.querySelector('._chevron');
if (chevron) {
chevron.classList.toggle('_rotated');
}
}
};
// Theme management
const applyTheme = (theme) => {
currentTheme = theme;
localStorage.setItem('duofarmer_theme', theme);
const elements = [
document.getElementById("_container"),
document.getElementById("_floating_btn")
];
elements.forEach(el => {
if (el) {
el.className = el.className.replace(/theme-\w+/, `theme-${theme}`);
}
});
// Update theme selector
document.querySelectorAll('._theme_option').forEach(btn => {
btn.classList.toggle('_active', btn.dataset.theme === theme);
});
};
// Event listeners
const addEventListeners = () => {
// Floating button
document.getElementById('_floating_btn')?.addEventListener('click', (e) => {
// Create ripple effect
const button = e.currentTarget;
const ripple = button.querySelector('._btn_ripple');
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.style.transform = 'scale(0)';
ripple.style.opacity = '0.7';
setTimeout(() => {
ripple.style.transform = 'scale(4)';
ripple.style.opacity = '0';
}, 10);
toggleInterface();
});
// Header controls
document.getElementById('_minimize_btn')?.addEventListener('click', () => {
setInterfaceVisible(false);
});
document.getElementById('_close_btn')?.addEventListener('click', () => {
if (isRunning) {
if (confirm('Farming is active. Are you sure you want to close?')) {
stopFarming();
setInterfaceVisible(false);
}
} else {
setInterfaceVisible(false);
}
});
document.getElementById('_settings_btn')?.addEventListener('click', () => {
document.getElementById('_settings_modal').style.display = 'flex';
});
// Join button
document.getElementById('_join_btn')?.addEventListener('click', () => {
window.open('https://discord.gg/Gvmd7deFtS', '_blank');
localStorage.setItem('duofarmer_joined', 'true');
hasJoined = true;
toggleSection('_join_section');
document.getElementById('_join_status').textContent = '✓ Joined';
document.getElementById('_join_status').className = '_status_success';
document.getElementById('_main_content').style.display = 'block';
// Initialize in background
initializeFarming();
});
// Farming controls
document.getElementById('_start_farming')?.addEventListener('click', startFarming);
document.getElementById('_stop_farming')?.addEventListener('click', stopFarming);
document.getElementById('_pause_farming')?.addEventListener('click', pauseFarming);
// Thread slider
const slider = document.getElementById('_thread_slider');
const display = document.getElementById('_thread_display');
slider?.addEventListener('input', (e) => {
if (display) display.textContent = e.target.value;
updateActiveThreadsDisplay();
});
// Refresh button
document.getElementById('_refresh_btn')?.addEventListener('click', async () => {
const button = document.getElementById('_refresh_btn');
if (button) button.classList.add('_loading');
await refreshUserData();
if (button) button.classList.remove('_loading');
});
// Console controls
document.getElementById('_clear_console')?.addEventListener('click', () => {
const console = document.getElementById('_console_output');
if (console) console.innerHTML = '';
});
document.getElementById('_auto_scroll')?.addEventListener('click', (e) => {
e.currentTarget.classList.toggle('_active');
});
// Settings modal
document.getElementById('_close_modal')?.addEventListener('click', () => {
document.getElementById('_settings_modal').style.display = 'none';
});
document.getElementById('_settings_modal')?.addEventListener('click', (e) => {
if (e.target.classList.contains('_modal_overlay')) {
document.getElementById('_settings_modal').style.display = 'none';
}
});
// Theme selection
document.querySelectorAll('._theme_option').forEach(btn => {
btn.addEventListener('click', () => {
applyTheme(btn.dataset.theme);
});
});
// Settings controls
document.getElementById('_delay_slider')?.addEventListener('input', (e) => {
const value = e.target.value;
document.getElementById('_delay_value').textContent = `${value}ms`;
});
// Stats reset
document.getElementById('_reset_stats')?.addEventListener('click', resetStats);
// Export stats
document.getElementById('_export_stats')?.addEventListener('click', exportStats);
};
// Farming functions
const startFarming = async () => {
if (isRunning) return;
const mode = document.getElementById('_farming_mode')?.value;
const threadCount = parseInt(document.getElementById('_thread_slider')?.value || '3');
isRunning = true;
farmingStats.startTime = Date.now();
farmingStats.sessions = 0;
farmingStats.errors = 0;
// Update UI
document.getElementById('_start_farming').style.display = 'none';
document.getElementById('_stop_farming').style.display = 'inline-flex';
document.getElementById('_pause_farming').style.display = 'inline-flex';
logToConsole(`Starting ${mode} with ${threadCount} threads`, 'success');
// Start farming timer
const timer = setInterval(updateFarmingTime, 1000);
try {
switch (mode) {
case 'xp_multi':
await startMultiThreadedFarming('xp', threadCount);
break;
case 'gem_multi':
await startMultiThreadedFarming('gem', threadCount);
break;
case 'mixed_farming':
await startMixedFarming(threadCount);
break;
case 'streak_repair':
await repairStreak();
break;
case 'streak_farm':
await farmStreakLoop();
break;
default:
logToConsole(`Unknown farming mode: ${mode}`, 'error');
}
} catch (error) {
logToConsole(`Farming error: ${error.message}`, 'error');
} finally {
clearInterval(timer);
}
};
const stopFarming = () => {
if (!isRunning) return;
isRunning = false;
// Stop all threads
activeThreads.forEach(thread => thread.stop());
activeThreads = [];
// Update UI
document.getElementById('_start_farming').style.display = 'inline-flex';
document.getElementById('_stop_farming').style.display = 'none';
document.getElementById('_pause_farming').style.display = 'none';
// Clear progress bars
const threadBars = document.getElementById('_thread_bars');
if (threadBars) threadBars.innerHTML = '';
updateActiveThreadsDisplay();
logToConsole('Farming stopped', 'info');
};
const pauseFarming = () => {
// Toggle pause state
const button = document.getElementById('_pause_farming');
const isPaused = button.textContent.includes('Resume');
if (isPaused) {
// Resume farming
activeThreads.forEach(thread => thread.isActive = true);
button.innerHTML = '<span class="_btn_content"><span class="_btn_icon">⏨</span><span class="_btn_text">Pause</span></span>';
logToConsole('Farming resumed', 'info');
} else {
// Pause farming
activeThreads.forEach(thread => thread.isActive = false);
button.innerHTML = '<span class="_btn_content"><span class="_btn_icon">▶</span><span class="_btn_text">Resume</span></span>';
logToConsole('Farming paused', 'warning');
}
};
const startMultiThreadedFarming = async (type, threadCount) => {
activeThreads = [];
for (let i = 1; i <= threadCount; i++) {
const worker = new FarmingWorker(i, type);
activeThreads.push(worker);
// Start thread with slight delay to avoid rate limiting
setTimeout(() => {
if (isRunning) worker.start();
}, i * 100);
}
updateActiveThreadsDisplay();
// Wait for all threads to complete (they run until stopped)
await new Promise(resolve => {
const checkInterval = setInterval(() => {
if (!isRunning || activeThreads.every(thread => !thread.isActive)) {
clearInterval(checkInterval);
resolve();
}
}, 1000);
});
};
const startMixedFarming = async (threadCount) => {
activeThreads = [];
const types = ['xp', 'gem'];
for (let i = 1; i <= threadCount; i++) {
const type = types[(i - 1) % types.length];
const worker = new FarmingWorker(i, type);
activeThreads.push(worker);
setTimeout(() => {
if (isRunning) worker.start();
}, i * 150);
}
updateActiveThreadsDisplay();
await new Promise(resolve => {
const checkInterval = setInterval(() => {
if (!isRunning || activeThreads.every(thread => !thread.isActive)) {
clearInterval(checkInterval);
resolve();
}
}, 1000);
});
};
const updateActiveThreadsDisplay = () => {
const display = document.getElementById('_active_threads');
if (display) {
display.textContent = activeThreads.filter(thread => thread.isActive).length;
}
};
// Original farming functions (adapted for multi-threading)
const getJwtToken = () => {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.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(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join("")
);
return JSON.parse(jsonPayload);
};
const formatHeaders = (jwt) => ({
"Content-Type": "application/json",
Authorization: "Bearer " + jwt,
"User-Agent": navigator.userAgent,
});
const getUserInfo = async (sub) => {
const userInfoUrl = `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,level,numFollowers,numFollowing,gems,creationDate,streakData`;
const response = await fetch(userInfoUrl, {
method: "GET",
headers: defaultHeaders,
});
return await response.json();
};
const sendRequestWithDefaultHeaders = async ({ url, payload, headers = {}, method = "GET" }) => {
const mergedHeaders = { ...defaultHeaders, ...headers };
return await fetch(url, {
method,
headers: mergedHeaders,
body: payload ? JSON.stringify(payload) : undefined,
});
};
const farmXpOnce = async () => {
const startTime = Math.floor(Date.now() / 1000);
const fromLanguage = userInfo.fromLanguage;
const completeUrl = `https://stories.duolingo.com/api2/stories/en-${fromLanguage}-the-passport/complete`;
const payload = {
awardXp: true,
isFeaturedStoryInPracticeHub: false,
completedBonusChallenge: true,
mode: "READ",
isV2Redo: false,
isV2Story: false,
isLegendaryMode: true,
masterVersion: false,
maxScore: 0,
numHintsUsed: 0,
score: 0,
startTime: startTime,
fromLanguage: fromLanguage,
learningLanguage: "en",
hasXpBoost: false,
happyHourBonusXp: 449,
};
return await sendRequestWithDefaultHeaders({
url: completeUrl,
payload: payload,
method: "POST",
});
};
const farmGemOnce = async () => {
const idReward = "SKILL_COMPLETION_BALANCED-dd2495f4_d44e_3fc3_8ac8_94e2191506f0-2-GEMS";
const patchUrl = `https://www.duolingo.com/2017-06-30/users/${sub}/rewards/${idReward}`;
const patchData = {
consumed: true,
learningLanguage: userInfo.learningLanguage,
fromLanguage: userInfo.fromLanguage,
};
return await sendRequestWithDefaultHeaders({
url: patchUrl,
payload: patchData,
method: "PATCH",
});
};
const repairStreak = async () => {
logToConsole('Starting streak repair...', 'info');
try {
if (!userInfo.streakData?.currentStreak) {
logToConsole('No streak to repair!', 'error');
return;
}
const startStreakDate = userInfo.streakData.currentStreak.startDate;
const endStreakDate = userInfo.streakData.currentStreak.endDate;
const startStreakTimestamp = Math.floor(new Date(startStreakDate).getTime() / 1000);
const endStreakTimestamp = Math.floor(new Date(endStreakDate).getTime() / 1000);
const expectedStreak = Math.floor((endStreakTimestamp - startStreakTimestamp) / (60 * 60 * 24)) + 1;
if (expectedStreak > userInfo.streak) {
logToConsole(`Found ${expectedStreak - userInfo.streak} frozen days. Repairing...`, 'warning');
let currentTimestamp = Math.floor(Date.now() / 1000);
for (let i = 0; i < expectedStreak && isRunning; i++) {
await farmSessionOnce(currentTimestamp, currentTimestamp + 60);
currentTimestamp -= 86400;
logToConsole(`Repaired day ${i + 1}/${expectedStreak}`, 'info');
await delay(DELAY);
}
const updatedUser = await getUserInfo(sub);
if (updatedUser.streak >= expectedStreak) {
logToConsole(`Streak repair completed! New streak: ${updatedUser.streak}`, 'success');
userInfo = updatedUser;
totalEarned.streak += (updatedUser.streak - userInfo.streak);
updateUserInfo();
updateEarnedStats();
}
} else {
logToConsole('No frozen streak detected', 'info');
}
} catch (error) {
logToConsole(`Streak repair failed: ${error.message}`, 'error');
} finally {
stopFarming();
}
};
const farmStreakLoop = async () => {
logToConsole('Starting streak farming...', 'info');
const hasStreak = !!userInfo.streakData?.currentStreak;
const startStreakDate = hasStreak ? userInfo.streakData.currentStreak.startDate : new Date();
const startFarmStreakTimestamp = Math.floor(new Date(startStreakDate).getTime() / 1000);
let currentTimestamp = hasStreak ? startFarmStreakTimestamp - 86400 : startFarmStreakTimestamp;
while (isRunning) {
try {
await farmSessionOnce(currentTimestamp, currentTimestamp + 60);
currentTimestamp -= 86400;
totalEarned.streak++;
userInfo.streak++;
updateUserInfo();
updateEarnedStats();
logToConsole(`Streak increased to ${userInfo.streak}`, 'success');
await delay(DELAY);
} catch (error) {
logToConsole(`Streak farming error: ${error.message}`, 'error');
await delay(DELAY * 2);
}
}
};
const farmSessionOnce = async (startTime, endTime) => {
const sessionPayload = {
challengeTypes: [
"assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect",
"characterTrace", "characterWrite", "completeReverseTranslation", "definition",
"dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse",
"gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name",
"listenComprehension", "listenIsolation", "listenSpeak", "listenTap",
"orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete",
"radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize",
"radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select",
"selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap",
"syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete",
"tapCompleteTable", "tapDescribe", "translate", "transliterate",
"transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete",
"typeCompleteTable", "writeComprehension",
],
fromLanguage: userInfo.fromLanguage,
isFinalLevel: false,
isV2: true,
juicy: true,
learningLanguage: userInfo.learningLanguage,
smartTipsVersion: 2,
type: "GLOBAL_PRACTICE",
};
const sessionRes = await sendRequestWithDefaultHeaders({
url: "https://www.duolingo.com/2017-06-30/sessions",
payload: sessionPayload,
method: "POST",
});
const sessionData = await sessionRes.json();
const updateSessionPayload = {
...sessionData,
heartsLeft: 0,
startTime: startTime,
enableBonusPoints: false,
endTime: endTime,
failed: false,
maxInLessonStreak: 9,
shouldLearnThings: true,
};
const updateRes = await sendRequestWithDefaultHeaders({
url: `https://www.duolingo.com/2017-06-30/sessions/${sessionData.id}`,
payload: updateSessionPayload,
method: "PUT",
});
return await updateRes.json();
};
// UI update functions
const updateUserInfo = () => {
if (!userInfo) return;
const elements = {
username: document.getElementById('_username'),
fromLang: document.getElementById('_from_lang'),
learningLang: document.getElementById('_learning_lang'),
currentStreak: document.getElementById('_current_streak'),
currentGems: document.getElementById('_current_gems'),
currentXp: document.getElementById('_current_xp')
};
if (elements.username) elements.username.textContent = userInfo.username;
if (elements.fromLang) elements.fromLang.textContent = userInfo.fromLanguage;
if (elements.learningLang) elements.learningLang.textContent = userInfo.learningLanguage;
if (elements.currentStreak) elements.currentStreak.textContent = userInfo.streak?.toLocaleString() || '0';
if (elements.currentGems) elements.currentGems.textContent = userInfo.gems?.toLocaleString() || '0';
if (elements.currentXp) elements.currentXp.textContent = userInfo.totalXp?.toLocaleString() || '0';
};
const refreshUserData = async () => {
if (!sub || !defaultHeaders) return;
try {
logToConsole('Refreshing user data...', 'info');
userInfo = await getUserInfo(sub);
updateUserInfo();
logToConsole('User data refreshed successfully', 'success');
} catch (error) {
logToConsole(`Failed to refresh user data: ${error.message}`, 'error');
}
};
const resetStats = () => {
totalEarned = { xp: 0, gems: 0, streak: 0 };
farmingStats = { sessions: 0, errors: 0, startTime: null };
updateEarnedStats();
logToConsole('Statistics reset', 'info');
};
const exportStats = () => {
const stats = {
totalEarned,
farmingStats,
userInfo: userInfo ? {
username: userInfo.username,
streak: userInfo.streak,
gems: userInfo.gems,
totalXp: userInfo.totalXp
} : null,
exportDate: new Date().toISOString(),
version: VERSION
};
const blob = new Blob([JSON.stringify(stats, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `duofarmer-stats-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
logToConsole('Statistics exported successfully', 'success');
};
// Initialization
const initializeFarming = async () => {
try {
jwt = getJwtToken();
if (!jwt) {
logToConsole('Please login to Duolingo and reload the page', 'error');
return false;
}
defaultHeaders = formatHeaders(jwt);
const decodedJwt = decodeJwtToken(jwt);
sub = decodedJwt.sub;
logToConsole('Loading user information...', 'info');
userInfo = await getUserInfo(sub);
if (userInfo && userInfo.username) {
updateUserInfo();
logToConsole(`Welcome ${userInfo.username}! Ready to farm.`, 'success');
return true;
} else {
logToConsole('Failed to load user information', 'error');
return false;
}
} catch (error) {
logToConsole(`Initialization error: ${error.message}`, 'error');
return false;
}
};
// Main initialization
(async () => {
try {
// Initialize interface
initInterface();
setInterfaceVisible(false);
// Apply saved theme
applyTheme(currentTheme);
// Add event listeners
addEventListeners();
if (hasJoined) {
// Initialize in background without blocking
initializeFarming();
}
logToConsole('DuoNexus initialized successfully', 'success');
logToConsole('Created by tw1sk - Discord: @tw1sk', 'info');
} catch (error) {
console.error('Initialization failed:', error);
}
})();