Duolingo Super Manager - Create referral links, activate Super, manage accounts
// ==UserScript==
// @name Duolingo Auto-Super
// @namespace http://tampermonkey.net/
// @version 3.4
// @description Duolingo Super Manager - Create referral links, activate Super, manage accounts
// @author MeowWoof
// @match https://www.duolingo.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @connect duolingo-super022.vercel.app
// @run-at document-idle
// @license MIT
// @antifeature payment Requires a private key purchased via Discord for full access
// @icon https://d35aaqx5ub95lt.cloudfront.net/vendor/38dc6a042b0de3f6aeb44ff2aa70de73.svg
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_VERSION = '3.4';
const API_BASE = 'https://duolingo-super022.vercel.app';
const FREE_KEY = 'DS-FREE-PUBLIC-KEY-2025';
const _COUP = 'DUOBNBJUNE2026';
const DISCORD_URL = 'https://discord.gg/ufBrcGemBH';
const CD_STORE = 'duods_cd_expire';
const VER_CHECK_STORE = 'duods_ver_ts';
const POLL_INTERVAL = 30000;
let _sess = null;
let _keyData = null;
let _cdTimer = null;
let _pollTimer = null;
let _hist = [];
let _hidden = false;
let _animLock = false;
let _pgLock = false;
let _curPg = 'auth';
let _ntTimer;
let _currentType = 'free';
let _expireTimer = null;
function _isDark() {
const h = document.documentElement;
if (h.dataset.theme === 'dark') return true;
if (h.dataset.theme === 'light') return false;
const bg = window.getComputedStyle(document.body).backgroundColor;
if (bg) {
const m = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (m) {
const l = 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3];
if (l < 80) return true;
if (l > 160) return false;
}
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function _applyTheme() {
const r = document.getElementById('ds-root');
if (!r) return;
_isDark() ? r.setAttribute('data-dark', '1') : r.removeAttribute('data-dark');
}
function _watchTheme() {
_applyTheme();
const obs = new MutationObserver(_applyTheme);
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
obs.observe(document.body, { attributes: true, attributeFilter: ['data-theme', 'class'] });
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', _applyTheme);
setInterval(_applyTheme, 5000);
}
function _req(path, method, body) {
return new Promise(resolve => {
const hdrs = _hdrs();
if (!hdrs && path !== '/api/key/info' && path !== '/api/admin/version') return resolve({ error: 'Unauthorized' });
const cfg = {
method: method || 'POST',
url: API_BASE + path,
headers: hdrs || { 'Content-Type': 'application/json' },
timeout: 90000,
onload: r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve({ error: 'Parse error' }); } },
onerror: () => resolve({ error: 'Network error' }),
ontimeout: () => resolve({ error: 'Timeout' })
};
if (body) cfg.data = JSON.stringify(body);
GM_xmlhttpRequest(cfg);
});
}
function _hdrs() {
if (!_sess) return null;
if (Date.now() > _sess.exp) { _sess = null; return null; }
return { 'Content-Type': 'application/json', 'x-ds-key': _sess.key, 'x-ds-token': _sess.tok };
}
function _fetchKey(key) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: `${API_BASE}/api/key/info?key=${encodeURIComponent(key)}`,
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
onload: r => {
try {
const d = JSON.parse(r.responseText);
if (d.ok) {
_sess = { key, tok: d.session_token || _fnv(key), exp: Date.now() + (d.session_ttl_ms || 3600000) };
}
resolve(d);
} catch { resolve({ error: 'Parse error' }); }
},
onerror: () => resolve({ error: 'Network error' }),
ontimeout: () => resolve({ error: 'Timeout' })
});
});
}
function _checkVersion() {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET', url: `${API_BASE}/api/admin/version`,
headers: { 'Content-Type': 'application/json' }, timeout: 15000,
onload: r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve(null); } },
onerror: () => resolve(null), ontimeout: () => resolve(null)
});
});
}
function _fnv(s) {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h * 0x01000193) >>> 0; }
return h.toString(16).padStart(8, '0') + Date.now().toString(36);
}
const _set = (k, v) => GM_setValue(k, v);
const _get = (k, d) => GM_getValue(k, d);
function _tsGMT7() {
const now = new Date(Date.now() + 7 * 3600000);
return `${String(now.getUTCHours()).padStart(2,'0')}:${String(now.getUTCMinutes()).padStart(2,'0')}:${String(now.getUTCSeconds()).padStart(2,'0')}`;
}
GM_addStyle(`
#ds-root {
--c-bg: #ffffff;
--c-surface: #f7f7f7;
--c-border: #e5e5e5;
--c-text: #3c3c3c;
--c-muted: #afafaf;
--c-blue: #1cb0f3;
--c-green: #58cc02;
--c-red: #ff4b4b;
--c-amber: #ffc800;
--c-purple: #ce82ff;
--c-shadow: rgba(0,0,0,0.08);
}
#ds-root[data-dark] {
--c-bg: #131f24;
--c-surface: #1a2e38;
--c-border: #2a404d;
--c-text: #ffffff;
--c-muted: #7a9aaa;
--c-blue: #1cb0f3;
--c-green: #58cc02;
--c-red: #ff4b4b;
--c-amber: #ffc800;
--c-purple: #ce82ff;
--c-shadow: rgba(0,0,0,0.40);
}
#ds-root * { box-sizing: border-box; }
#ds-root p, #ds-root span, #ds-root button,
#ds-root input, #ds-root label, #ds-root div {
font-family: inherit !important;
}
#ds-root p, #ds-root span { margin: 0; padding: 0; }
#ds-root a { text-decoration: none; }
#ds-root svg { flex-shrink: 0; }
.ds-shell {
position: fixed; right: 16px; bottom: 16px;
z-index: 2147483647;
display: flex; flex-direction: column;
align-items: flex-end; gap: 8px;
}
@media (max-width: 699px) { .ds-shell { margin-bottom: 72px; } }
.ds-card {
width: 300px;
background: var(--c-bg);
border: 2px solid var(--c-border);
border-radius: 20px;
box-shadow: 0 4px 0 var(--c-border), 0 8px 24px var(--c-shadow);
overflow: hidden;
max-height: 88vh;
display: flex;
flex-direction: column;
}
.ds-toggle {
display: flex; align-items: center; gap: 6px;
padding: 7px 13px; border-radius: 20px;
border: 2px solid var(--c-border);
border-bottom: 3px solid var(--c-border);
background: var(--c-bg); cursor: pointer;
transition: transform .08s, border-bottom-width .08s, box-shadow .08s;
box-shadow: 0 2px 0 var(--c-border);
}
.ds-toggle:hover {
transform: translateY(2px);
border-bottom-width: 2px;
box-shadow: none;
}
.ds-toggle:active {
transform: translateY(3px);
border-bottom-width: 1px;
box-shadow: none;
}
.ds-toggle-label {
font-size: 13px; font-weight: 800;
color: var(--c-muted); user-select: none;
}
.ds-page { display: none; }
.ds-page.active {
display: flex; flex-direction: column;
overflow-y: auto; max-height: 88vh;
}
.ds-page.active::-webkit-scrollbar { width: 0; }
.ds-section { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.ds-divider { height: 2px; background: var(--c-border); flex-shrink: 0; }
.ds-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px; border-bottom: 2px solid var(--c-border);
flex-shrink: 0;
}
.ds-header-title { font-size: 14px; font-weight: 800; color: var(--c-text); }
.ds-header-sub { font-size: 11px; font-weight: 600; color: var(--c-muted); margin-top: 1px; }
.ds-label { font-size: 10px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--c-muted); }
.ds-small { font-size: 12px; font-weight: 600; color: var(--c-muted); line-height: 1.45; }
.ds-mono { font-family: monospace !important; }
.ds-row { display: flex; align-items: center; gap: 7px; }
.ds-col { display: flex; flex-direction: column; gap: 7px; }
.ds-between { display: flex; align-items: center; justify-content: space-between; }
.ds-badge {
display: inline-flex; align-items: center;
padding: 3px 10px; border-radius: 99px;
font-size: 10px; font-weight: 800; letter-spacing: .6px;
user-select: none; flex-shrink: 0;
}
.ds-badge-free { background: #d7ffb8; color: #3c7a00; }
.ds-badge-paid { background: #ddf4ff; color: #0e7da8; }
.ds-badge-event { background: #fff0a0; color: #a06800; }
.ds-badge-admin { background: #f1e4ff; color: #7a3daf; }
#ds-root[data-dark] .ds-badge-free { background: #1a3d00; color: #58cc02; }
#ds-root[data-dark] .ds-badge-paid { background: #003d52; color: #1cb0f3; }
#ds-root[data-dark] .ds-badge-event { background: #ffc800; color: #3c2800; }
#ds-root[data-dark] .ds-badge-admin { background: #2e0052; color: #ce82ff; }
.ds-input-wrap {
display: flex; align-items: center; gap: 8px;
border: 2px solid var(--c-border); border-radius: 12px;
padding: 0 12px; background: var(--c-surface); height: 42px;
transition: border-color .15s;
}
.ds-input-wrap:focus-within { border-color: var(--c-blue); }
.ds-input {
flex: 1; border: none; background: none; outline: none;
font-size: 13px; font-weight: 700; color: var(--c-text);
}
.ds-input::placeholder { color: var(--c-muted); font-weight: 600; }
.ds-btn {
display: flex; align-items: center; justify-content: center; gap: 6px;
height: 42px; padding: 0 14px; border-radius: 12px;
border: 2px solid transparent;
border-bottom: 3px solid transparent;
font-size: 14px; font-weight: 800;
cursor: pointer; user-select: none;
transition: transform .08s, border-bottom-width .08s, box-shadow .08s, filter .08s;
position: relative;
}
.ds-btn:hover:not(:disabled) {
transform: translateY(2px);
border-bottom-width: 2px !important;
box-shadow: none !important;
filter: brightness(.97);
}
.ds-btn:active:not(:disabled) {
transform: translateY(3px) !important;
border-bottom-width: 1px !important;
box-shadow: none !important;
filter: brightness(.94) !important;
}
.ds-btn:disabled { opacity: .38; cursor: not-allowed; }
.ds-btn-primary {
background: var(--c-blue); color: #fff;
border-color: #0e86b8; border-bottom-color: #0a6a94;
box-shadow: 0 2px 0 #0a6a94;
}
.ds-btn-outline {
background: var(--c-bg);
border-color: var(--c-border); border-bottom-color: #c8c8c8;
color: var(--c-text);
box-shadow: 0 2px 0 #c8c8c8;
}
.ds-btn-ghost {
background: var(--c-surface);
border-color: var(--c-border); border-bottom-color: #c8c8c8;
color: var(--c-text);
box-shadow: 0 2px 0 #c8c8c8;
}
.ds-btn-danger {
background: transparent;
border-color: var(--c-red); border-bottom-color: #c83030;
color: var(--c-red);
box-shadow: 0 2px 0 #c83030;
}
.ds-btn-green {
background: var(--c-green); color: #fff;
border-color: #3d8f00; border-bottom-color: #2d6b00;
box-shadow: 0 2px 0 #2d6b00;
}
.ds-btn-purple {
background: var(--c-purple); color: #fff;
border-color: #9e52d4; border-bottom-color: #7a3aac;
box-shadow: 0 2px 0 #7a3aac;
}
.ds-btn-full { width: 100%; }
.ds-btn-sm { height: 34px; font-size: 12px; padding: 0 10px; border-radius: 10px; }
#ds-root[data-dark] .ds-btn-outline { border-bottom-color: #1a3040; box-shadow: 0 2px 0 #1a3040; }
#ds-root[data-dark] .ds-btn-ghost { border-bottom-color: #1a3040; box-shadow: 0 2px 0 #1a3040; }
.ds-progress-wrap {
height: 8px; background: var(--c-surface);
border-radius: 99px; overflow: hidden;
border: 2px solid var(--c-border);
}
.ds-progress-fill {
height: 100%; border-radius: 99px;
background: var(--c-blue); transition: width .5s cubic-bezier(.34,1.56,.64,1);
}
.ds-progress-fill.warn { background: var(--c-red); }
.ds-log {
background: var(--c-surface); border: 2px solid var(--c-border);
border-radius: 12px; padding: 8px 10px;
font-size: 11px; font-family: monospace !important;
font-weight: 600; color: var(--c-muted);
max-height: 130px; overflow-y: auto; line-height: 1.65;
white-space: pre-wrap; word-break: break-all;
}
.ds-log::-webkit-scrollbar { width: 3px; }
.ds-log::-webkit-scrollbar-thumb { background: var(--c-border); border-radius: 3px; }
.lo { color: var(--c-green); }
.le { color: var(--c-red); }
.li { color: var(--c-blue); }
.lw { color: var(--c-amber); }
.ds-tabs {
display: flex; border-bottom: 2px solid var(--c-border);
padding: 0 6px; gap: 2px; flex-shrink: 0;
}
.ds-tab {
flex: 1; padding: 8px 4px; border: none; background: none;
font-size: 13px; font-weight: 800; color: var(--c-muted);
cursor: pointer; border-bottom: 3px solid transparent;
margin-bottom: -2px; transition: color .15s, border-color .15s;
}
.ds-tab.on { color: var(--c-blue); border-bottom-color: var(--c-blue); }
.ds-tab:hover:not(.on) { color: var(--c-text); }
.ds-pane { display: none; }
.ds-pane.active { display: flex; flex-direction: column; }
.ds-cd {
display: none; align-items: center; justify-content: space-between;
padding: 8px 12px;
background: #fff8e0; border: 2px solid #ffc800; border-radius: 10px;
}
#ds-root[data-dark] .ds-cd { background: #2a2200; border-color: #a06800; }
.ds-cd.on { display: flex; animation: ds-slidein .2s cubic-bezier(.34,1.56,.64,1); }
.ds-cd-label { font-size: 11px; font-weight: 800; color: #a06800; display: flex; align-items: center; gap: 4px; }
#ds-root[data-dark] .ds-cd-label { color: var(--c-amber); }
.ds-cd-time { font-size: 13px; font-weight: 800; color: #a06800; font-family: monospace !important; }
#ds-root[data-dark] .ds-cd-time { color: var(--c-amber); }
.ds-err { font-size: 11px; font-weight: 700; color: var(--c-red); min-height: 14px; }
.ds-spinner { display: none; justify-content: center; align-items: center; gap: 4px; }
.ds-spinner.on { display: flex; }
.ds-dot {
width: 5px; height: 5px; border-radius: 50%;
background: var(--c-muted);
animation: ds-pulse 1.1s ease-in-out infinite;
}
.ds-dot:nth-child(2) { animation-delay: .16s; }
.ds-dot:nth-child(3) { animation-delay: .32s; }
@keyframes ds-pulse {
0%, 80%, 100% { opacity: .25; transform: scale(.6); }
40% { opacity: 1; transform: scale(1); }
}
@keyframes ds-slidein {
from { opacity: 0; transform: translateY(-4px) scale(.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes ds-popin {
from { opacity: 0; transform: scale(.93); }
to { opacity: 1; transform: scale(1); }
}
.ds-copy-tag {
display: inline-flex; align-items: center;
padding: 1px 6px; border-radius: 6px; margin-left: 4px;
font-size: 10px; font-weight: 800; cursor: pointer;
background: var(--c-surface);
border: 2px solid var(--c-border);
color: var(--c-muted);
transition: transform .08s, box-shadow .08s;
vertical-align: middle;
box-shadow: 0 2px 0 var(--c-border);
}
.ds-copy-tag:hover { transform: translateY(1px); box-shadow: none; }
.ds-copy-tag:active { transform: translateY(2px); box-shadow: none; }
.ds-ntf-shell {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
z-index: 2147483647; pointer-events: none;
}
.ds-ntf-box {
background: var(--c-bg);
border: 2px solid var(--c-border);
border-bottom: 4px solid var(--c-border);
border-radius: 16px; padding: 10px 14px;
box-shadow: 0 4px 0 var(--c-border), 0 8px 24px var(--c-shadow);
opacity: 0; transform: translateY(10px) scale(.96); pointer-events: auto;
transition: opacity .22s cubic-bezier(.34,1.56,.64,1), transform .22s cubic-bezier(.34,1.56,.64,1);
min-width: 200px; max-width: 280px;
}
.ds-ntf-box.show { opacity: 1; transform: translateY(0) scale(1); }
.ds-ntf-title { font-size: 13px; font-weight: 800; color: var(--c-text); }
.ds-ntf-body { font-size: 11px; font-weight: 600; color: var(--c-muted); margin-top: 2px; }
.ds-key-display {
font-size: 12px; font-weight: 800; color: var(--c-text);
font-family: monospace !important;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-expiry {
display: none; align-items: center; gap: 5px;
padding: 7px 11px;
background: #fff8e0; border: 2px solid #ffc800; border-radius: 10px;
animation: ds-slidein .2s cubic-bezier(.34,1.56,.64,1);
}
#ds-root[data-dark] .ds-expiry { background: #2a2200; border-color: #a06800; }
.ds-expiry.on { display: flex; }
.ds-expiry-txt { font-size: 11px; font-weight: 800; color: #a06800; }
#ds-root[data-dark] .ds-expiry-txt { color: var(--c-amber); }
.ds-quota-row { display: flex; flex-direction: column; gap: 5px; }
`);
const _root = document.createElement('div');
_root.id = 'ds-root';
_root.innerHTML = `
<div class="ds-ntf-shell" id="ds-ntf">
<div class="ds-ntf-box" id="ds-ntf-box">
<p class="ds-ntf-title" id="ds-ntf-title"></p>
<p class="ds-ntf-body" id="ds-ntf-body"></p>
</div>
</div>
<div class="ds-shell" id="ds-shell">
<button class="ds-toggle" id="ds-toggle">
<svg id="ds-ico-show" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--c-muted);display:none;">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<svg id="ds-ico-hide" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--c-muted);">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
<span class="ds-toggle-label" id="ds-toggle-label">Hide</span>
</button>
<div class="ds-card" id="ds-card">
<!-- AUTH PAGE -->
<div class="ds-page active" id="ds-page-auth">
<div class="ds-header">
<div>
<p class="ds-header-title">DuoDS</p>
<p class="ds-header-sub">Enter your key to continue</p>
</div>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-muted)" stroke-width="1.5">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<div class="ds-section ds-col" style="gap:8px;">
<div class="ds-row">
<div class="ds-input-wrap" style="flex:1;">
<input id="ds-key-in" class="ds-input ds-mono" type="password" placeholder="DS-XXXXXXXXXXXXXXXX" autocomplete="off">
</div>
<button class="ds-btn ds-btn-primary" id="ds-unlock-btn" style="flex-shrink:0;">Unlock</button>
</div>
<div class="ds-spinner" id="ds-auth-spin"><div class="ds-dot"></div><div class="ds-dot"></div><div class="ds-dot"></div></div>
<p class="ds-err" id="ds-auth-err"></p>
<button class="ds-btn ds-btn-green ds-btn-full" id="ds-free-btn">Use Free Tier</button>
<div class="ds-divider"></div>
<div class="ds-between">
<span class="ds-small">Need a key?</span>
<a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Discord →</a>
</div>
<div class="ds-between" style="padding-top:2px;">
<span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
<span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
</div>
</div>
</div>
<!-- DASH PAGE -->
<div class="ds-page" id="ds-page-dash">
<div class="ds-header">
<div style="min-width:0;flex:1;">
<div class="ds-row" style="gap:6px;">
<p class="ds-key-display" id="ds-key-disp">DS-XXXX...</p>
<span class="ds-badge ds-badge-free" id="ds-tier-tag" style="display:none;">FREE</span>
</div>
<p class="ds-small" id="ds-key-meta" style="margin-top:2px;">Batch: ? · CD: ?s</p>
</div>
<button class="ds-btn ds-btn-sm ds-btn-danger" id="ds-chkey-btn">Change</button>
</div>
<div class="ds-section ds-col" style="gap:6px;padding-bottom:8px;">
<div class="ds-expiry" id="ds-expiry">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--c-amber)" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
<span class="ds-expiry-txt" id="ds-expiry-txt">Expires in --:--</span>
</div>
<div class="ds-quota-row" id="ds-quota-wrap">
<div class="ds-between">
<span class="ds-label">Daily quota</span>
<span class="ds-small ds-mono" id="ds-quota-val">0 / 0</span>
</div>
<div class="ds-progress-wrap">
<div class="ds-progress-fill" id="ds-quota-fill" style="width:0%"></div>
</div>
</div>
<div class="ds-cd" id="ds-cd">
<span class="ds-cd-label">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
Cooldown
</span>
<span class="ds-cd-time" id="ds-cd-time">00:00</span>
</div>
</div>
<div class="ds-divider"></div>
<div class="ds-tabs">
<button class="ds-tab on" data-tab="link">Links</button>
<button class="ds-tab" data-tab="active">Activate</button>
<button class="ds-tab" data-tab="hist">History</button>
</div>
<div class="ds-pane active ds-section ds-col" id="ds-pane-link" style="gap:8px;">
<p class="ds-small">
Create <span id="ds-batch-n" style="font-weight:700;color:var(--c-text);">?</span> links per batch.
</p>
<button class="ds-btn ds-btn-primary ds-btn-full" id="ds-create-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<span id="ds-create-lbl">Create Batch</span>
</button>
<div class="ds-progress-wrap" id="ds-create-prog" style="display:none;">
<div class="ds-progress-fill" id="ds-create-fill"></div>
</div>
<div class="ds-log" id="ds-log-link">Ready...</div>
</div>
<div class="ds-pane ds-section ds-col" id="ds-pane-active" style="gap:8px;">
<button class="ds-btn ds-btn-primary ds-btn-full" id="ds-activate-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
<span id="ds-activate-lbl">Activate Super (Family)</span>
</button>
<button class="ds-btn ds-btn-purple ds-btn-full" id="ds-coupon-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 12V22H4V12"/><path d="M22 7H2v5h20V7z"/><path d="M12 22V7"/></svg>
<span id="ds-coupon-lbl">Get 1 Month Free</span>
</button>
<div class="ds-log" id="ds-log-active">Waiting...</div>
</div>
<div class="ds-pane ds-section ds-col" id="ds-pane-hist" style="gap:8px;">
<div class="ds-row">
<button class="ds-btn ds-btn-ghost ds-btn-full" id="ds-loadhist-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.68"/></svg>
<span id="ds-loadhist-lbl">Load</span>
</button>
<button class="ds-btn ds-btn-outline ds-btn-sm" id="ds-copyall-btn">Copy all</button>
</div>
<div class="ds-log" id="ds-hist-log" style="max-height:180px;">Press load...</div>
</div>
<div class="ds-divider"></div>
<div class="ds-section ds-between" style="padding-top:8px;padding-bottom:10px;">
<span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
<span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
</div>
</div>
<!-- CHANGE KEY PAGE -->
<div class="ds-page" id="ds-page-chkey">
<div class="ds-header">
<div>
<p class="ds-header-title">Switch Key</p>
<p class="ds-header-sub">Enter new key or go back to free tier</p>
</div>
</div>
<div class="ds-section ds-col" style="gap:8px;">
<div class="ds-row">
<div class="ds-input-wrap" style="flex:1;">
<input id="ds-newkey-in" class="ds-input ds-mono" type="password" placeholder="DS-XXXXXXXXXXXXXXXX" autocomplete="off">
</div>
<button class="ds-btn ds-btn-primary" id="ds-newkey-btn" style="flex-shrink:0;">Apply</button>
</div>
<div class="ds-spinner" id="ds-chkey-spin"><div class="ds-dot"></div><div class="ds-dot"></div><div class="ds-dot"></div></div>
<p class="ds-err" id="ds-chkey-err"></p>
<button class="ds-btn ds-btn-green ds-btn-full" id="ds-activefree-btn">Use Free Tier</button>
<div class="ds-divider"></div>
<div class="ds-between">
<span class="ds-small">Get a premium key</span>
<a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Discord →</a>
</div>
<button class="ds-btn ds-btn-outline ds-btn-full" id="ds-back-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back
</button>
<div class="ds-between" style="padding-top:2px;">
<span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
<span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
</div>
</div>
</div>
<!-- BLOCKED PAGE -->
<div class="ds-page" id="ds-page-blocked">
<div class="ds-header">
<div>
<p class="ds-header-title" style="color:var(--c-red);">Key In Use</p>
<p class="ds-header-sub">This key is active in another session</p>
</div>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-red)" stroke-width="1.5">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
<line x1="9" y1="15" x2="15" y2="19"/><line x1="15" y1="15" x2="9" y2="19"/>
</svg>
</div>
<div class="ds-section ds-col" style="gap:8px;">
<p class="ds-small">Use a different key or contact Discord to get your own private key.</p>
<button class="ds-btn ds-btn-danger ds-btn-full" id="ds-blocked-chkey-btn">Change Key</button>
<div class="ds-divider"></div>
<div class="ds-between">
<span class="ds-small">Need support?</span>
<a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Join Discord</a>
</div>
<div class="ds-between" style="padding-top:2px;">
<span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
<span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
</div>
</div>
</div>
<!-- UPDATE PAGE -->
<div class="ds-page" id="ds-page-update">
<div class="ds-header">
<div>
<p class="ds-header-title" style="color:var(--c-amber);">Update Required</p>
<p class="ds-header-sub">Your version is outdated</p>
</div>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-amber)" stroke-width="1.5">
<circle cx="12" cy="12" r="9"/>
<path d="M12 7v6"/><circle cx="12" cy="16.5" r="1" fill="var(--c-amber)"/>
</svg>
</div>
<div class="ds-section ds-col" style="gap:8px;">
<p class="ds-small" id="ds-update-msg">Script is outdated. Please update to the latest version to continue.</p>
<a class="ds-btn ds-btn-primary ds-btn-full" id="ds-update-btn" href="#" target="_blank" rel="noopener" style="text-decoration:none;">Update Now</a>
<div class="ds-between" style="padding-top:2px;">
<span class="ds-small" style="color:var(--c-border)">Current: v${SCRIPT_VERSION}</span>
</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(_root);
_watchTheme();
function _notify(title, body, dur = 5) {
const box = document.getElementById('ds-ntf-box');
document.getElementById('ds-ntf-title').textContent = title;
document.getElementById('ds-ntf-body').textContent = body;
box.classList.add('show');
clearTimeout(_ntTimer);
_ntTimer = setTimeout(() => box.classList.remove('show'), dur * 1000);
}
function _toggleVis(hide) {
if (_animLock) return;
_animLock = true;
_hidden = hide;
const card = document.getElementById('ds-card');
const shell = document.getElementById('ds-shell');
const lbl = document.getElementById('ds-toggle-label');
const icoH = document.getElementById('ds-ico-hide');
const icoS = document.getElementById('ds-ico-show');
const h = card.getBoundingClientRect().height || 300;
shell.style.transition = card.style.transition = '.55s cubic-bezier(.16,1,.32,1)';
if (hide) {
shell.style.bottom = `-${h - 6}px`;
card.style.opacity = '0';
lbl.textContent = 'Show';
icoH.style.display = 'none';
icoS.style.display = '';
} else {
shell.style.bottom = '14px';
card.style.opacity = '';
lbl.textContent = 'Hide';
icoH.style.display = '';
icoS.style.display = 'none';
}
setTimeout(() => { shell.style.transition = card.style.transition = ''; _animLock = false; }, 560);
}
document.getElementById('ds-toggle').addEventListener('click', () => _toggleVis(!_hidden));
function _goPage(to) {
if (_pgLock || _curPg === to) return;
_pgLock = true;
const fromEl = document.getElementById(`ds-page-${_curPg}`);
const toEl = document.getElementById(`ds-page-${to}`);
if (!fromEl || !toEl) { _pgLock = false; return; }
const card = document.getElementById('ds-card');
const oldH = card.offsetHeight;
toEl.style.cssText = `display:flex;flex-direction:column;position:absolute;visibility:hidden;pointer-events:none;width:${card.clientWidth}px;`;
card.appendChild(toEl);
const newH = toEl.scrollHeight;
toEl.style.cssText = '';
card.style.height = oldH + 'px';
card.style.transition = 'height .4s cubic-bezier(.16,1,.32,1)';
fromEl.style.transition = 'opacity .2s';
fromEl.style.opacity = '0';
requestAnimationFrame(() => { card.style.height = newH + 'px'; });
setTimeout(() => {
fromEl.classList.remove('active');
fromEl.style.cssText = '';
toEl.classList.add('active');
_curPg = to;
setTimeout(() => { card.style.height = ''; card.style.transition = ''; _pgLock = false; }, 420);
}, 220);
}
document.querySelectorAll('#ds-root .ds-tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#ds-root .ds-tab').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
['link', 'active', 'hist'].forEach(t => {
const el = document.getElementById(`ds-pane-${t}`);
if (el) el.classList.toggle('active', btn.dataset.tab === t);
});
if (btn.dataset.tab === 'hist' && _sess) {
const key = _get('dds_key', '');
if (key && !_hist.length) {
_req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET').then(r => {
if (r.ok && r.links) { _hist = r.links; _renderHist(); }
});
}
}
});
});
function _log(id, msg, type = 'i') {
const el = document.getElementById(id);
if (!el) return;
const cls = { o: 'lo', e: 'le', i: 'li', w: 'lw' }[type] || 'li';
const span = document.createElement('span');
span.className = cls;
span.textContent = `[${_tsGMT7()}] ${msg}`;
el.appendChild(span);
el.appendChild(document.createTextNode('\n'));
el.scrollTop = el.scrollHeight;
}
function _logClear(id) {
const el = document.getElementById(id);
if (el) el.textContent = '';
}
function _logErr(id, msg) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = '';
const s = document.createElement('span');
s.className = 'le';
s.textContent = msg;
el.appendChild(s);
}
function _setBusy(btnId, spinId, on, label) {
const btn = document.getElementById(btnId);
const spin = spinId ? document.getElementById(spinId) : null;
if (!btn) return;
btn.disabled = on;
if (spin) spin.classList.toggle('on', on);
const lbl = btn.querySelector('span[id]') || btn.querySelector('span');
if (lbl && label) { if (on) { lbl._orig = lbl.textContent; lbl.textContent = label; } else if (lbl._orig) { lbl.textContent = lbl._orig; lbl._orig = null; } }
}
function _refreshQuota(used, limit) {
const pct = limit > 0 ? Math.min((used / limit) * 100, 100) : 0;
const fill = document.getElementById('ds-quota-fill');
const val = document.getElementById('ds-quota-val');
if (fill) { fill.style.width = pct + '%'; fill.classList.toggle('warn', pct > 85); }
if (val) val.textContent = `${used} / ${limit}`;
}
function _saveCD(sec) { if (sec > 0) _set(CD_STORE, Date.now() + sec * 1000); }
function _loadCD() {
const exp = _get(CD_STORE, 0);
if (!exp) return 0;
const rem = Math.ceil((exp - Date.now()) / 1000);
return rem > 0 ? rem : 0;
}
function _startCD(sec, save = true) {
if (_cdTimer) clearInterval(_cdTimer);
let rem = Math.ceil(sec);
if (save) _saveCD(rem);
const cdEl = document.getElementById('ds-cd');
const cdTx = document.getElementById('ds-cd-time');
const cBtn = document.getElementById('ds-create-btn');
if (cdEl) cdEl.classList.add('on');
if (cBtn) cBtn.disabled = true;
function tick() {
if (rem <= 0) {
clearInterval(_cdTimer);
if (cdEl) cdEl.classList.remove('on');
if (cBtn) cBtn.disabled = false;
return;
}
if (cdTx) cdTx.textContent = `${String(Math.floor(rem/60)).padStart(2,'0')}:${String(rem%60).padStart(2,'0')}`;
rem--;
}
tick();
_cdTimer = setInterval(tick, 1000);
}
function _startExpireCountdown(seconds) {
if (_expireTimer) clearInterval(_expireTimer);
const banner = document.getElementById('ds-expiry');
const txt = document.getElementById('ds-expiry-txt');
if (!banner || !txt) return;
let rem = Math.ceil(seconds);
if (rem <= 0 || rem > 3600) { banner.classList.remove('on'); return; }
banner.classList.add('on');
function tick() {
if (rem <= 0) { clearInterval(_expireTimer); banner.classList.remove('on'); _onKeyExpired(); return; }
const hh = Math.floor(rem/3600);
const mm = String(Math.floor((rem%3600)/60)).padStart(2,'0');
const ss = String(rem%60).padStart(2,'0');
txt.textContent = hh > 0 ? `Expires in ${hh}:${mm}:${ss}` : `Expires in ${mm}:${ss}`;
rem--;
}
tick();
_expireTimer = setInterval(tick, 1000);
}
function _onKeyExpired() {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
if (_cdTimer) { clearInterval(_cdTimer); _cdTimer = null; }
if (_expireTimer) { clearInterval(_expireTimer); _expireTimer = null; }
_sess = null; _keyData = null; _currentType = 'free';
_set('dds_key', '');
_notify('Key Expired', 'Switched to free tier.', 6);
_fetchKey(FREE_KEY).then(r => {
if (r.ok && _sess) { _set('dds_key', FREE_KEY); _applyKey(r, FREE_KEY); _goPage('dash'); _startPoll(FREE_KEY); }
else { _sess = null; _goPage('auth'); }
});
}
function _tierLabel(type) {
return { free: 'FR', paid: 'PR', event: 'EV', admin: 'AD' }[type] || type.toUpperCase();
}
function _applyTierFx(newType) {
const tag = document.getElementById('ds-tier-tag');
if (!tag) return;
tag.className = `ds-badge ds-badge-${newType}`;
tag.textContent = _tierLabel(newType);
tag.style.display = '';
}
function _applyKey(data, key) {
_keyData = data;
_currentType = data.key_type || 'paid';
const disp = document.getElementById('ds-key-disp');
const meta = document.getElementById('ds-key-meta');
const batchN = document.getElementById('ds-batch-n');
if (disp) disp.textContent = key.slice(0, 16) + '...';
if (batchN) batchN.textContent = data.batch_size;
let expStr = '';
if (data.expires_in_seconds != null) {
const h = Math.floor(data.expires_in_seconds / 3600);
const m = Math.floor((data.expires_in_seconds % 3600) / 60);
expStr = ` · Exp: ${h}h${m}m`;
_startExpireCountdown(data.expires_in_seconds);
} else {
const banner = document.getElementById('ds-expiry');
if (banner) banner.classList.remove('on');
if (_expireTimer) { clearInterval(_expireTimer); _expireTimer = null; }
}
if (meta) meta.textContent = `Batch: ${data.batch_size} · CD: ${data.cd_seconds}s · ${data.used_total}/${data.total_limit}${expStr}`;
_refreshQuota(data.used_today || 0, data.daily_limit);
_applyTierFx(_currentType);
const cdRemain = (data.cooldown_remain && data.cooldown_remain > 0) ? data.cooldown_remain : _loadCD();
if (cdRemain > 0) _startCD(cdRemain, false);
}
function _startPoll(key) {
if (_pollTimer) clearInterval(_pollTimer);
_pollTimer = setInterval(async () => {
if (!_sess) return;
const r = await _fetchKey(key);
if (!r.ok) return;
if (r.in_use) {
clearInterval(_pollTimer); _pollTimer = null;
_sess = null; _goPage('blocked');
_notify('Key In Use', 'Another session is using this key.', 8);
return;
}
if (r.expires_in_seconds != null && r.expires_in_seconds <= 0) {
clearInterval(_pollTimer); _pollTimer = null;
_onKeyExpired(); return;
}
const savedKey = _get('dds_key', '');
if (savedKey === FREE_KEY && r.key_type === 'event' && r.event_key) {
_set('dds_key', r.event_key); _sess = null;
const ev = await _fetchKey(r.event_key);
if (ev.ok && _sess) { _applyKey(ev, r.event_key); _notify('Upgraded to EVENT', 'Free key upgraded to EVENT tier!', 6); }
return;
}
if (r.key_type !== _currentType) {
_applyKey(r, savedKey || key);
_notify('Key Updated', `Tier: ${_tierLabel(r.key_type)}`, 4);
} else if (r.expires_in_seconds != null) {
_startExpireCountdown(r.expires_in_seconds);
}
}, POLL_INTERVAL);
}
async function _unlock(key, errId, spinId, btnId, lblId) {
const errEl = document.getElementById(errId);
if (!key) return;
_sess = null;
_setBusy(btnId, spinId, true, 'Checking...');
if (errEl) errEl.textContent = '';
const r = await _fetchKey(key);
_setBusy(btnId, spinId, false);
if (r.in_use) {
_sess = null;
if (errEl) errEl.textContent = 'Key in use by another session.';
return;
}
if (r.ok && _sess) {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
_set('dds_key', key);
_applyKey(r, key);
_goPage('dash');
_startPoll(key);
_notify('Unlocked', `Welcome, ${r.label || 'user'}!`);
} else {
_sess = null;
if (errEl) errEl.textContent = 'Invalid key: ' + (r.error || 'Unknown error');
}
}
document.getElementById('ds-unlock-btn').addEventListener('click', () => {
_unlock(document.getElementById('ds-key-in').value.trim(), 'ds-auth-err', 'ds-auth-spin', 'ds-unlock-btn', 'ds-unlock-lbl');
});
document.getElementById('ds-key-in').addEventListener('keydown', e => {
if (e.key === 'Enter') _unlock(document.getElementById('ds-key-in').value.trim(), 'ds-auth-err', 'ds-auth-spin', 'ds-unlock-btn', 'ds-unlock-lbl');
});
document.getElementById('ds-free-btn').addEventListener('click', () => {
_unlock(FREE_KEY, 'ds-auth-err', 'ds-auth-spin', 'ds-free-btn', null);
});
document.getElementById('ds-chkey-btn').addEventListener('click', () => {
document.getElementById('ds-newkey-in').value = '';
document.getElementById('ds-chkey-err').textContent = '';
_goPage('chkey');
});
document.getElementById('ds-back-btn').addEventListener('click', () => { _goPage('dash'); });
document.getElementById('ds-newkey-btn').addEventListener('click', () => {
_unlock(document.getElementById('ds-newkey-in').value.trim(), 'ds-chkey-err', 'ds-chkey-spin', 'ds-newkey-btn', null);
});
document.getElementById('ds-newkey-in').addEventListener('keydown', e => {
if (e.key === 'Enter') _unlock(document.getElementById('ds-newkey-in').value.trim(), 'ds-chkey-err', 'ds-chkey-spin', 'ds-newkey-btn', null);
});
document.getElementById('ds-activefree-btn').addEventListener('click', async () => {
_sess = null;
const cdSaved = _loadCD();
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
_setBusy('ds-activefree-btn', null, true, 'Activating...');
const r = await _fetchKey(FREE_KEY);
_setBusy('ds-activefree-btn', null, false);
if (r.ok && _sess) {
_set('dds_key', FREE_KEY);
_applyKey(r, FREE_KEY);
if (cdSaved > 0) _startCD(cdSaved, false);
_goPage('dash');
_startPoll(FREE_KEY);
_notify('Free Tier Active', 'Now using the free public key.');
} else {
_sess = null;
document.getElementById('ds-chkey-err').textContent = 'Could not activate free key. Try again.';
}
});
document.getElementById('ds-blocked-chkey-btn').addEventListener('click', () => {
_set('dds_key', '');
_goPage('chkey');
});
document.getElementById('ds-create-btn').addEventListener('click', async () => {
if (!_sess) return;
const key = _get('dds_key', '');
if (!key) return;
const btn = document.getElementById('ds-create-btn');
const prog = document.getElementById('ds-create-prog');
const fill = document.getElementById('ds-create-fill');
btn.disabled = true;
prog.style.display = '';
fill.style.width = '20%';
_logClear('ds-log-link');
_log('ds-log-link', `Creating ${_keyData?.batch_size || '?'} links...`, 'i');
fill.style.transition = 'width .8s';
fill.style.width = '60%';
const r = await _req('/api/link/create', 'POST', { key });
fill.style.width = '100%';
setTimeout(() => { prog.style.display = 'none'; fill.style.width = '0%'; fill.style.transition = ''; }, 500);
btn.disabled = false;
if (r.ok && r.urls && r.urls.length > 0) {
_log('ds-log-link', `Created ${r.count} link${r.failed > 0 ? ` (${r.failed} failed)` : ''}`, 'o');
r.urls.forEach((url, i) => { _log('ds-log-link', `#${i+1} ${url}`, 'o'); _hist.unshift(url); });
if (_keyData) { _keyData.used_today = (_keyData.used_today || 0) + r.count; _refreshQuota(_keyData.used_today, _keyData.daily_limit); }
if (_keyData?.cd_seconds) _startCD(_keyData.cd_seconds);
} else {
const msg = r.error || 'Unknown error';
_log('ds-log-link', `Error: ${msg}`, 'e');
const cdM = msg.match(/Cooldown.*?(\d+):(\d+)/);
if (cdM) _startCD(parseInt(cdM[1])*60+parseInt(cdM[2]));
else if (r.cooldown_remain > 0) _startCD(r.cooldown_remain);
}
});
function _getJwtUid() {
let jwt = '';
try { const m = document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)')); if (m) jwt = decodeURIComponent(m[2]); } catch {}
if (!jwt) return { jwt: null, uid: null };
try {
const seg = jwt.split('.')[1];
const pad = seg + '='.repeat((4 - seg.length % 4) % 4);
const uid = String(JSON.parse(atob(pad)).sub || '');
return { jwt, uid };
} catch { return { jwt: null, uid: null }; }
}
async function _activateFamilyDirect(jwt, uid, invite_code) {
const url = `https://www.duolingo.com/2017-06-30/users/${uid}/family-plan/members/invite/${invite_code}`;
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'POST', url,
headers: {
'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json; charset=UTF-8', 'Origin': 'https://www.duolingo.com',
'Referer': `https://www.duolingo.com/family-plan?invite=${invite_code}`,
'User-Agent': navigator.userAgent, 'X-Requested-With': 'XMLHttpRequest',
'X-Amzn-Trace-Id': `User=${uid}`
},
data: '{}', timeout: 15000,
onload: r => {
if (r.status === 200 || r.status === 201) {
try {
const d = JSON.parse(r.responseText);
const secs = (d.secondaryMembers || []).map(String);
resolve({ ok: true, joined: secs.includes(String(uid)) });
} catch { resolve({ ok: true, joined: true }); }
} else if (r.status === 409) {
resolve({ ok: true, already_member: true });
} else if (r.status === 400) {
resolve({ ok: false, error: 'Account not eligible (previously used Super)' });
} else {
resolve({ ok: false, error: `Duolingo returned ${r.status}` });
}
},
onerror: () => resolve({ ok: false, error: 'Network error' }),
ontimeout: () => resolve({ ok: false, error: 'Timeout' })
});
});
}
document.getElementById('ds-activate-btn').addEventListener('click', async () => {
if (!_sess) return;
const { jwt, uid } = _getJwtUid();
if (!jwt || !uid) return _log('ds-log-active', 'JWT not found. Please log into Duolingo first!', 'e');
_setBusy('ds-activate-btn', null, true, 'Activating...');
if (!_hist.length) {
const key = _get('dds_key', '');
if (!key) { _setBusy('ds-activate-btn', null, false); return _log('ds-log-active', 'No key found.', 'e'); }
const lr = await _req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET');
if (lr.ok && lr.links && lr.links.length) {
_hist = lr.links;
} else {
_setBusy('ds-activate-btn', null, false);
return _log('ds-log-active', 'No links available. Create links first.', 'e');
}
}
const code = _hist[0].split('/').pop();
_log('ds-log-active', `UID: ${uid} · Code: ${code}`, 'i');
const r = await _activateFamilyDirect(jwt, uid, code);
_setBusy('ds-activate-btn', null, false);
if (r.ok) {
_log('ds-log-active', r.already_member ? 'Already has Super.' : 'Activated! Check the app.', 'o');
_notify('Super Activated', 'Free Super Duolingo activated!');
} else {
_log('ds-log-active', `Error: ${r.error || 'Failed'}`, 'e');
}
});
const _COUPON_CODE = 'DUOBNBJUNE2026';
async function _applyCouponDirect(jwt, uid) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'POST',
url: `https://www.duolingo.com/2023-05-23/users/${uid}/shop-items`,
headers: {
'Accept': 'application/json; charset=UTF-8', 'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json', 'Origin': 'https://www.duolingo.com',
'Referer': 'https://www.duolingo.com/settings/super', 'User-Agent': navigator.userAgent
},
data: JSON.stringify({ itemName: 'premium_subscription', couponCode: _COUPON_CODE, productId: 'com.duolingo.subscription.premium.prepaid', vendor: 'VENDOR_PREPAID' }),
timeout: 15000,
onload: r => {
if (r.status === 409) return resolve({ ok: true, already_active: true });
if (r.status === 200 || r.status === 201) return resolve({ ok: true });
try { const d = JSON.parse(r.responseText); resolve({ ok: false, error: d.error || d.message || `Status ${r.status}` }); }
catch { resolve({ ok: false, error: `Status ${r.status}` }); }
},
onerror: () => resolve({ ok: false, error: 'Network error' }),
ontimeout: () => resolve({ ok: false, error: 'Timeout' })
});
});
}
document.getElementById('ds-coupon-btn').addEventListener('click', async () => {
if (!_sess) return;
const { jwt, uid } = _getJwtUid();
if (!jwt || !uid) return _log('ds-log-active', 'JWT not found. Please log into Duolingo first!', 'e');
_setBusy('ds-coupon-btn', null, true, 'Processing...');
_log('ds-log-active', `Applying 1-month coupon for UID: ${uid}`, 'i');
const r = await _applyCouponDirect(jwt, uid);
_setBusy('ds-coupon-btn', null, false);
if (r.ok) {
_log('ds-log-active', r.already_active ? 'Already has active Super.' : '1 month Super activated! Check the app.', 'o');
_notify('1 Month Free!', 'Super Duolingo activated for 1 month!');
} else {
_log('ds-log-active', `Error: ${r.error || 'Failed'}`, 'e');
}
});
document.getElementById('ds-loadhist-btn').addEventListener('click', async () => {
if (!_sess) return;
const key = _get('dds_key', '');
if (!key) return;
_setBusy('ds-loadhist-btn', null, true, 'Loading...');
const r = await _req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET');
_setBusy('ds-loadhist-btn', null, false);
if (r.ok && r.links) {
_hist = r.links;
_renderHist();
_notify('Loaded', `${r.links.length} links.`, 3);
} else {
_logErr('ds-hist-log', `Error: ${r.error || 'Unknown'}`);
}
});
function _renderHist() {
const el = document.getElementById('ds-hist-log');
el.textContent = '';
if (!_hist.length) {
const s = document.createElement('span');
s.className = 'lw';
s.textContent = 'No links yet.';
el.appendChild(s);
return;
}
_hist.forEach((url, i) => {
const sUrl = document.createElement('span');
sUrl.className = 'lo';
sUrl.textContent = `#${String(i+1).padStart(3,'0')} ${url}`;
const sTag = document.createElement('span');
sTag.className = 'ds-copy-tag';
sTag.textContent = 'copy';
sTag.addEventListener('click', e => {
e.stopPropagation();
navigator.clipboard?.writeText(url).catch(() => {});
sTag.textContent = '✓';
setTimeout(() => { sTag.textContent = 'copy'; }, 1200);
});
el.appendChild(sUrl);
el.appendChild(sTag);
el.appendChild(document.createTextNode('\n'));
});
}
document.getElementById('ds-copyall-btn').addEventListener('click', () => {
if (!_hist.length) return;
navigator.clipboard?.writeText(_hist.join('\n')).catch(() => {});
const btn = document.getElementById('ds-copyall-btn');
const orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = orig; }, 1500);
});
(async function _boot() {
let vr = null;
const lastVerCheck = _get(VER_CHECK_STORE, 0);
if (Date.now() - lastVerCheck > 86400000) {
try { vr = await _checkVersion(); } catch {}
if (vr !== null) _set(VER_CHECK_STORE, Date.now());
}
if (vr && vr.ok && vr.current_version && vr.current_version !== SCRIPT_VERSION) {
const msg = document.getElementById('ds-update-msg');
if (msg) msg.textContent = `v${SCRIPT_VERSION} is outdated. Latest: v${vr.current_version}. Please update to continue.`;
const btn = document.getElementById('ds-update-btn');
if (btn && vr.update_url) btn.href = vr.update_url;
const fromEl = document.getElementById('ds-page-auth');
if (fromEl) fromEl.classList.remove('active');
const toEl = document.getElementById('ds-page-update');
if (toEl) toEl.classList.add('active');
_curPg = 'update';
_toggleVis(false);
return;
}
const saved = _get('dds_key', '');
const key = saved || FREE_KEY;
const card = document.getElementById('ds-card');
const shell = document.getElementById('ds-shell');
const h = card.scrollHeight || 300;
shell.style.bottom = `-${h - 6}px`;
card.style.opacity = '0';
_fetchKey(key).then(r => {
if (r.in_use) {
_set('dds_key', '');
_goPage('blocked');
} else if (r.ok && _sess) {
if (r.expires_in_seconds != null && r.expires_in_seconds <= 0) {
_set('dds_key', '');
_fetchKey(FREE_KEY).then(fr => {
if (fr.ok && _sess) { _set('dds_key', FREE_KEY); _applyKey(fr, FREE_KEY); _goPage('dash'); _startPoll(FREE_KEY); }
else { _sess = null; _goPage('auth'); }
});
return;
}
if (!saved) _set('dds_key', FREE_KEY);
_applyKey(r, key);
_goPage('dash');
_startPoll(key);
} else {
_sess = null;
if (key !== FREE_KEY) _set('dds_key', '');
document.getElementById('ds-key-in').value = saved || '';
}
});
_toggleVis(false);
setTimeout(() => {
shell.style.transition = card.style.transition = '.55s cubic-bezier(.16,1,.32,1)';
shell.style.bottom = '14px';
card.style.opacity = '';
setTimeout(() => { shell.style.transition = card.style.transition = ''; }, 560);
}, 400);
})();
})();