diff --git a/extension/duohacker-extension.zip b/extension/duohacker-extension.zip new file mode 100644 index 0000000..1f83ee1 Binary files /dev/null and b/extension/duohacker-extension.zip differ diff --git a/extension/source/content.js b/extension/source/content.js new file mode 100644 index 0000000..5e4d540 --- /dev/null +++ b/extension/source/content.js @@ -0,0 +1,10 @@ +const script = document.createElement('script'); + +script.src = chrome.runtime.getURL('inject.js'); + +script.onload = () => script.remove(); + +(document.head || document.documentElement) + .appendChild(script); + +console.log('[DuoHacker] loader injected'); \ No newline at end of file diff --git a/extension/source/icon.png b/extension/source/icon.png new file mode 100644 index 0000000..5552aec Binary files /dev/null and b/extension/source/icon.png differ diff --git a/extension/source/inject.js b/extension/source/inject.js new file mode 100644 index 0000000..3e3c55e --- /dev/null +++ b/extension/source/inject.js @@ -0,0 +1,298 @@ +(async () => { + +(() => { + + const prefix = 'usc:Duolingo_DuoHacker:'; + + const listeners = new Map(); + + let listenerSeq = 0; + + function read(name, fallback) { + + const raw = localStorage.getItem(prefix + name); + + if (raw === null) return fallback; + + try { + return JSON.parse(raw); + } catch { + return raw; + } + } + + function write(name, value) { + + const oldValue = read(name); + + localStorage.setItem( + prefix + name, + JSON.stringify(value) + ); + + for (const listener of listeners.values()) { + + if (listener.name === name) { + + listener.callback( + name, + oldValue, + value, + false + ); + } + } + } + + function GM_xmlhttpRequest(details) { + + const controller = new AbortController(); + + const promise = fetch(details.url, { + + method: details.method || 'GET', + + headers: details.headers, + + body: details.data, + + signal: controller.signal, + + credentials: + details.anonymous + ? 'omit' + : 'include' + + }) + + .then(async response => { + + const responseText = + + details.responseType === 'blob' || + details.responseType === 'arraybuffer' + + ? '' + + : await response.clone().text(); + + const body = + + details.responseType === 'blob' + + ? await response.blob() + + : details.responseType === 'arraybuffer' + + ? await response.arrayBuffer() + + : details.responseType === 'json' + + ? JSON.parse(responseText || 'null') + + : responseText; + + const xhr = { + + response: body, + + responseText, + + status: response.status, + + statusText: response.statusText, + + finalUrl: response.url + + }; + + details.onload?.(xhr); + + return xhr; + + }) + + .catch(error => { + + details.onerror?.(error); + + throw error; + + }); + + return { + + abort: () => controller.abort(), + + then: promise.then.bind(promise), + + catch: promise.catch.bind(promise), + + finally: promise.finally.bind(promise) + + }; + } + + function GM_addStyle(css) { + + const style = document.createElement('style'); + + style.textContent = css; + + document.head.appendChild(style); + + return style; + } + + const GM_info = { + + script: { + + name: 'Duolingo DuoHacker', + + description: + 'The #1 Duolingo hack', + + version: '2026.05.25' + + }, + + scriptHandler: + 'Chrome Extension Harness', + + version: '3.0' + + }; + + const GM = { + + info: GM_info, + + getValue: (name, fallback) => + Promise.resolve(read(name, fallback)), + + setValue: (name, value) => + Promise.resolve(write(name, value)), + + deleteValue: name => + Promise.resolve( + localStorage.removeItem(prefix + name) + ), + + listValues: () => + Promise.resolve( + Object.keys(localStorage) + + .filter(key => + key.startsWith(prefix) + ) + + .map(key => + key.slice(prefix.length) + ) + ), + + addValueChangeListener: + (name, callback) => { + + const id = ++listenerSeq; + + listeners.set(id, { + name, + callback + }); + + return id; + }, + + removeValueChangeListener: + id => listeners.delete(id), + + addStyle: GM_addStyle, + + xmlHttpRequest: details => + Promise.resolve( + GM_xmlhttpRequest(details) + ), + + xmlhttpRequest: details => + Promise.resolve( + GM_xmlhttpRequest(details) + ), + + registerMenuCommand: + () => undefined, + + setClipboard: text => + navigator.clipboard.writeText( + String(text || '') + ) + }; + + Object.assign(window, { + + GM, + + GM_info, + + GM_getValue: read, + + GM_setValue: write, + + GM_deleteValue: name => + localStorage.removeItem(prefix + name), + + GM_listValues: () => + + Object.keys(localStorage) + + .filter(key => + key.startsWith(prefix) + ) + + .map(key => + key.slice(prefix.length) + ), + + GM_addValueChangeListener: + GM.addValueChangeListener, + + GM_removeValueChangeListener: + GM.removeValueChangeListener, + + GM_addStyle, + + GM_xmlhttpRequest, + + GM_registerMenuCommand: + () => undefined, + + unsafeWindow: window + + }); + +})(); + +try { + + const res = await fetch( + 'https://raw.githubusercontent.com/not2pixel/DuoHacker/main/v2/duohacker-v2.user.js' + ); + + const code = await res.text(); + + eval(code); + + console.log( + '[DuoHacker] injected successfully' + ); + +} catch (err) { + + console.error( + '[DuoHacker] injection failed:', + err + ); +} + +})(); \ No newline at end of file diff --git a/extension/source/manifest.json b/extension/source/manifest.json new file mode 100644 index 0000000..e8be3a1 --- /dev/null +++ b/extension/source/manifest.json @@ -0,0 +1,62 @@ +{ + "manifest_version": 3, + + "name": "Duolingo DuoHacker v2", + + "version": "1.0", + + "description": "Duolingo DuoHacker — Browser Extension", + + "icons": { + "128": "icon.png" + }, + + "content_scripts": [ + { + "matches": [ + "https://*.duolingo.com/*", + "https://*.duolingo.cn/*" + ], + + "js": [ + "content.js" + ], + + "run_at": "document_end" + } + ], + + "host_permissions": [ + "https://*.duolingo.com/*", + "https://*.duolingo.cn/*", + + "https://raw.githubusercontent.com/*", + + "https://avatars.githubusercontent.com/*", + + "https://fonts.googleapis.com/*", + + "https://greasyfork.org/*", + + "https://api.twisk.fun/*", + + "https://stories.duolingo.com/*", + + "https://goals-api.duolingo.com/*", + + "https://duolingo-leaderboards-prod.duolingo.com/*" + ], + + "web_accessible_resources": [ + { + "resources": [ + "inject.js" + ], + + "matches": [ + "https://*.duolingo.com/*", + "https://*.duolingo.cn/*" + ] + } + ] +}