diff --git a/README.md b/README.md index 9aead7e..cbd8c6c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Streak keeper and XP farm for Duolingo. Never get demoted again! ### How to use + 1. [Fork this repository](https://github.com/rfoel/duolingo/fork) 2. Go to [Duolingo](https://www.duolingo.com) 3. While logged in, open the browser's console (Option (⌥) + Command (⌘) + J (on macOS) or Shift + CTRL + J (on Windows/Linux)) @@ -38,3 +39,19 @@ This repository can also "study" lessons for you. This will give you XP so you w - This project won't help with your daily or friend quests, it can only earn XP to move up the league rank; - This project won't do real lessons or stories, only practices, so it won't affect your learning path; + +## Running as a standalone script + +You can run this script outside GitHub if you want to. You can have an `.env` file with the `DUOLINGO_JWT` and run the script like so: + +``` +node --env-file=.env index.js +``` + +> Node v20.6.0 or later is needed to use the `--env-file` flag. + +You can also load the env in the terminal like so: + +``` +DUOLINGO_JWT=... node index.js +``` \ No newline at end of file diff --git a/index.js b/index.js index 3cce6e1..63b2d46 100644 --- a/index.js +++ b/index.js @@ -1,95 +1,127 @@ -const headers = { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.DUOLINGO_JWT}`, - "user-agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", -}; +try { + process.env.LESSONS = process.env.LESSONS ?? 1; -const { sub } = JSON.parse( - Buffer.from(process.env.DUOLINGO_JWT.split(".")[1], "base64").toString(), -); + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.DUOLINGO_JWT}`, + "user-agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + }; -const { fromLanguage, learningLanguage, xpGains } = await fetch( - `https://www.duolingo.com/2017-06-30/users/${sub}?fields=fromLanguage,learningLanguage,xpGains`, - { - headers, - }, -).then((response) => response.json()); + const { sub } = JSON.parse( + Buffer.from(process.env.DUOLINGO_JWT.split(".")[1], "base64").toString(), + ); -for (let i = 0; i < process.env.LESSONS; i++) { - const session = await fetch("https://www.duolingo.com/2017-06-30/sessions", { - body: JSON.stringify({ - challengeTypes: [ - "assist", - "characterIntro", - "characterMatch", - "characterPuzzle", - "characterSelect", - "characterTrace", - "completeReverseTranslation", - "definition", - "dialogue", - "form", - "freeResponse", - "gapFill", - "judge", - "listen", - "listenComplete", - "listenMatch", - "match", - "name", - "listenComprehension", - "listenIsolation", - "listenTap", - "partialListen", - "partialReverseTranslate", - "readComprehension", - "select", - "selectPronunciation", - "selectTranscription", - "syllableTap", - "syllableListenTap", - "speak", - "tapCloze", - "tapClozeTable", - "tapComplete", - "tapCompleteTable", - "tapDescribe", - "translate", - "typeCloze", - "typeClozeTable", - "typeCompleteTable", - ], - fromLanguage, - isFinalLevel: false, - isV2: true, - juicy: true, - learningLanguage, - skillId: xpGains.find((xpGain) => xpGain.skillId).skillId, - smartTipsVersion: 2, - type: "SPEAKING_PRACTICE", - }), - headers, - method: "POST", - }).then((response) => response.json()); - - const response = await fetch( - `https://www.duolingo.com/2017-06-30/sessions/${session.id}`, + const { fromLanguage, learningLanguage } = await fetch( + `https://www.duolingo.com/2017-06-30/users/${sub}?fields=fromLanguage,learningLanguage`, { - body: JSON.stringify({ - ...session, - heartsLeft: 0, - startTime: (+new Date() - 60000) / 1000, - enableBonusPoints: false, - endTime: +new Date() / 1000, - failed: false, - maxInLessonStreak: 9, - shouldLearnThings: true, - }), headers, - method: "PUT", }, ).then((response) => response.json()); - console.log({ xp: response.xpGain }); + let xp = 0; + for (let i = 0; i < process.env.LESSONS; i++) { + const session = await fetch( + "https://www.duolingo.com/2017-06-30/sessions", + { + body: JSON.stringify({ + 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, + isFinalLevel: false, + isV2: true, + juicy: true, + learningLanguage, + smartTipsVersion: 2, + type: "GLOBAL_PRACTICE", + }), + headers, + method: "POST", + }, + ).then((response) => response.json()); + + const response = await fetch( + `https://www.duolingo.com/2017-06-30/sessions/${session.id}`, + { + body: JSON.stringify({ + ...session, + heartsLeft: 0, + startTime: (+new Date() - 60000) / 1000, + enableBonusPoints: false, + endTime: +new Date() / 1000, + failed: false, + maxInLessonStreak: 9, + shouldLearnThings: true, + }), + headers, + method: "PUT", + }, + ).then((response) => response.json()); + + xp += response.xpGain; + } + + console.log(`🎉 You won ${xp} XP`); +} catch (error) { + console.log("❌ Something went wrong"); + if (error instanceof Error) { + console.log(error.message); + } }