Duolingo Auto Solver & Queue

Duolingo Auto Solver with automatic "Listening" lesson starter [2024]

  1. // ==UserScript==
  2. // @name Duolingo Auto Solver & Queue
  3. // @namespace discord @stocktown
  4. // @match https://*.duolingo.com/*
  5. // @grant none
  6. // @version 1.0
  7. // @author DEARLY
  8. // @description Duolingo Auto Solver with automatic "Listening" lesson starter [2024]
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. let solvingIntervalId;
  13. let isAutoMode = false;
  14. const debug = false;
  15.  
  16. function addButtons() {
  17. if (window.location.pathname === '/learn') {
  18. let button = document.querySelector('a[data-test="global-practice"]');
  19. if (button) {
  20. return;
  21. }
  22. }
  23.  
  24. const solveAllButton = document.getElementById("solveAllButton");
  25. if (solveAllButton !== null) {
  26. return;
  27. }
  28.  
  29. const original = document.querySelectorAll('[data-test="player-next"]')[0];
  30. if (original === undefined) {
  31. const startButton = document.querySelector('[data-test="start-button"]');
  32. if (startButton === null) {
  33. return;
  34. }
  35. const wrapper = startButton.parentNode;
  36. const solveAllButton = document.createElement('a');
  37. solveAllButton.className = startButton.className;
  38. solveAllButton.id = "solveAllButton";
  39. solveAllButton.innerText = "COMPLETE SKILL";
  40. solveAllButton.removeAttribute('href');
  41. solveAllButton.addEventListener('click', () => {
  42. solving();
  43. setInterval(() => {
  44. const startButton = document.querySelector('[data-test="start-button"]');
  45. if (startButton && startButton.innerText.startsWith("START")) {
  46. startButton.click();
  47. }
  48. }, 3000);
  49. startButton.click();
  50. });
  51. wrapper.appendChild(solveAllButton);
  52. } else {
  53. const wrapper = document.getElementsByClassName('_10vOG')[0];
  54. wrapper.style.display = "flex";
  55.  
  56. const solveCopy = document.createElement('button');
  57. const pauseCopy = document.createElement('button');
  58.  
  59. solveCopy.id = 'solveAllButton';
  60. solveCopy.innerHTML = solvingIntervalId ? 'PAUSE SOLVE' : 'SOLVE ALL';
  61. solveCopy.disabled = false;
  62. pauseCopy.innerHTML = 'SOLVE';
  63.  
  64. const buttonStyle = `
  65. min-width: 150px;
  66. font-size: 17px;
  67. border: none;
  68. border-bottom: 4px solid #58a700;
  69. border-radius: 18px;
  70. padding: 13px 16px;
  71. transform: translateZ(0);
  72. transition: filter .2s;
  73. font-weight: 700;
  74. letter-spacing: .8px;
  75. background: #55CD2E;
  76. color: #fff;
  77. margin-left: 20px;
  78. cursor: pointer;
  79. `;
  80.  
  81. solveCopy.style.cssText = buttonStyle;
  82. pauseCopy.style.cssText = buttonStyle;
  83.  
  84. [solveCopy, pauseCopy].forEach(button => {
  85. button.addEventListener("mousemove", () => {
  86. button.style.filter = "brightness(1.1)";
  87. });
  88. });
  89.  
  90. [solveCopy, pauseCopy].forEach(button => {
  91. button.addEventListener("mouseleave", () => {
  92. button.style.filter = "none";
  93. });
  94. });
  95.  
  96. original.parentElement.appendChild(pauseCopy);
  97. original.parentElement.appendChild(solveCopy);
  98.  
  99. solveCopy.addEventListener('click', solving);
  100. pauseCopy.addEventListener('click', solve);
  101. }
  102. }
  103.  
  104. function solving() {
  105. if (solvingIntervalId) {
  106. clearInterval(solvingIntervalId);
  107. solvingIntervalId = undefined;
  108. document.getElementById("solveAllButton").innerText = "SOLVE ALL";
  109. isAutoMode = false;
  110. } else {
  111. document.getElementById("solveAllButton").innerText = "PAUSE SOLVE";
  112. isAutoMode = true;
  113. solvingIntervalId = setInterval(solve, 500);
  114. }
  115. }
  116.  
  117. function solve() {
  118. const selAgain = document.querySelectorAll('[data-test="player-practice-again"]');
  119. const practiceAgain = document.querySelector('[data-test="player-practice-again"]');
  120. if (selAgain.length === 1 && isAutoMode) {
  121.  
  122. selAgain[0].click();
  123.  
  124. return;
  125.  
  126. }
  127. if (practiceAgain !== null && isAutoMode) {
  128. practiceAgain.click();
  129. return;
  130. }
  131. try {
  132. window.sol = findReact(document.getElementsByClassName('_3FiYg')[0]).props.currentChallenge;
  133. } catch {
  134. let next = document.querySelector('[data-test="player-next"]');
  135. if (next) {
  136. next.click();
  137. }
  138. return;
  139. }
  140. if (!window.sol) {
  141. return;
  142. }
  143. let nextButton = document.querySelector('[data-test="player-next"]');
  144. if (!nextButton) {
  145. return;
  146. }
  147. if (document.querySelectorAll('[data-test*="challenge-speak"]').length > 0) {
  148. if (debug)
  149. document.getElementById("solveAllButton").innerText = 'Challenge Speak';
  150. const buttonSkip = document.querySelector('button[data-test="player-skip"]');
  151. if (buttonSkip) {
  152. buttonSkip.click();
  153. }
  154. } else if (window.sol.type === 'listenMatch') {
  155. if (debug)
  156. document.getElementById("solveAllButton").innerText = 'Listen Match';
  157. const nl = document.querySelectorAll('[data-test$="challenge-tap-token"]');
  158. window.sol.pairs?.forEach((pair) => {
  159. for (let i = 0; i < nl.length; i++) {
  160. let nlInnerText;
  161. if (nl[i].querySelectorAll('[data-test="challenge-tap-token-text"]').length > 1) {
  162. nlInnerText = nl[i].querySelector('[data-test="challenge-tap-token-text"]').innerText.toLowerCase().trim();
  163. } else {
  164. nlInnerText = findSubReact(nl[i]).text.toLowerCase().trim();
  165. }
  166. if (
  167. (
  168. nlInnerText === pair.learningWord.toLowerCase().trim() ||
  169. nlInnerText === pair.translation.toLowerCase().trim()
  170. ) &&
  171. !nl[i].disabled
  172. ) {
  173. nl[i].click();
  174. }
  175. }
  176. });
  177. } else if (document.querySelectorAll('[data-test="challenge-choice"]').length > 0) {
  178. // choice challenge
  179. if (debug)
  180. document.getElementById("solveAllButton").innerText = 'Challenge Choice';
  181. if (window.sol.correctTokens !== undefined) {
  182. correctTokensRun();
  183. nextButton.click()
  184. } else if (window.sol.correctIndex !== undefined) {
  185. document.querySelectorAll('[data-test="challenge-choice"]')[window.sol.correctIndex].click();
  186. nextButton.click();
  187. }
  188. } else if (document.querySelectorAll('[data-test$="challenge-tap-token"]').length > 0) {
  189.  
  190. if (window.sol.pairs !== undefined) {
  191. if (debug)
  192. document.getElementById("solveAllButton").innerText = 'Pairs';
  193. let nl = document.querySelectorAll('[data-test$="challenge-tap-token"]');
  194. if (document.querySelectorAll('[data-test="challenge-tap-token-text"]').length
  195. === nl.length) {
  196. window.sol.pairs?.forEach((pair) => {
  197. for (let i = 0; i < nl.length; i++) {
  198. const nlInnerText = nl[i].querySelector('[data-test="challenge-tap-token-text"]').innerText.toLowerCase().trim();
  199. try {
  200. if (
  201. (
  202. nlInnerText === pair.transliteration.toLowerCase().trim() ||
  203. nlInnerText === pair.character.toLowerCase().trim()
  204. )
  205. && !nl[i].disabled
  206. ) {
  207. nl[i].click()
  208. }
  209. } catch (TypeError) {
  210. if (
  211. (
  212. nlInnerText === pair.learningToken.toLowerCase().trim() ||
  213. nlInnerText === pair.fromToken.toLowerCase().trim()
  214. )
  215. && !nl[i].disabled
  216. ) {
  217. nl[i].click()
  218. }
  219. }
  220. }
  221. })
  222. }
  223. } else if (window.sol.correctTokens !== undefined) {
  224. if (debug)
  225. document.getElementById("solveAllButton").innerText = 'Token Run';
  226. correctTokensRun();
  227. nextButton.click()
  228. } else if (window.sol.correctIndices !== undefined) {
  229. if (debug)
  230. document.getElementById("solveAllButton").innerText = 'Indices Run';
  231. correctIndicesRun();
  232. }
  233. } else if (document.querySelectorAll('[data-test="challenge-tap-token-text"]').length > 0) {
  234. if (debug)
  235. document.getElementById("solveAllButton").innerText = 'Challenge Tap Token Text';
  236.  
  237. correctIndicesRun();
  238. } else if (document.querySelectorAll('[data-test="challenge-text-input"]').length > 0) {
  239. if (debug)
  240. document.getElementById("solveAllButton").innerText = 'Challenge Text Input';
  241. let elm = document.querySelectorAll('[data-test="challenge-text-input"]')[0];
  242. let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
  243. nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0] : (window.sol.displayTokens ? window.sol.displayTokens.find(t => t.isBlank).text : window.sol.prompt));
  244. let inputEvent = new Event('input', {
  245. bubbles: true
  246. });
  247.  
  248. elm.dispatchEvent(inputEvent);
  249. } else if (document.querySelectorAll('[data-test*="challenge-partialReverseTranslate"]').length > 0) {
  250. if (debug)
  251. document.getElementById("solveAllButton").innerText = 'Partial Reverse';
  252. let elm = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
  253. let nativeInputNodeTextSetter = Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set
  254. nativeInputNodeTextSetter.call(elm, '"' + window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join()?.replaceAll(',', '') + '"');
  255. let inputEvent = new Event('input', {
  256. bubbles: true
  257. });
  258.  
  259. elm.dispatchEvent(inputEvent);
  260. } else if (document.querySelectorAll('textarea[data-test="challenge-translate-input"]').length > 0) {
  261. if (debug)
  262. document.getElementById("solveAllButton").innerText = 'Challenge Translate Input';
  263. const elm = document.querySelector('textarea[data-test="challenge-translate-input"]');
  264. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
  265. nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0] : window.sol.prompt);
  266.  
  267. let inputEvent = new Event('input', {
  268. bubbles: true
  269. });
  270.  
  271. elm.dispatchEvent(inputEvent);
  272. }
  273. nextButton.click()
  274. }
  275.  
  276. function correctTokensRun() {
  277. const all_tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
  278. const correct_tokens = window.sol.correctTokens;
  279. const clicked_tokens = [];
  280. correct_tokens.forEach(correct_token => {
  281. const matching_elements = Array.from(all_tokens).filter(element => element.textContent.trim() === correct_token.trim());
  282. if (matching_elements.length > 0) {
  283. const match_index = clicked_tokens.filter(token => token.textContent.trim() === correct_token.trim()).length;
  284. if (match_index < matching_elements.length) {
  285. matching_elements[match_index].click();
  286. clicked_tokens.push(matching_elements[match_index]);
  287. } else {
  288. clicked_tokens.push(matching_elements[0]);
  289. }
  290. }
  291. });
  292. }
  293.  
  294. function correctIndicesRun() {
  295. if (window.sol.correctIndices) {
  296. window.sol.correctIndices?.forEach(index => {
  297. document.querySelectorAll('div[data-test="word-bank"] [data-test="challenge-tap-token-text"]')[index].click();
  298. });
  299. // nextButton.click();
  300. }
  301. }
  302.  
  303. function findSubReact(dom, traverseUp = 0) {
  304. const key = Object.keys(dom).find(key => key.startsWith("__reactProps$"));
  305. return dom.parentElement[key].children.props;
  306. }
  307.  
  308. function findReact(dom, traverseUp = 0) {
  309. let reactProps = Object.keys(dom.parentElement).find((key) => key.startsWith("__reactProps$"));
  310. while (traverseUp-- > 0 && dom.parentElement) {
  311. dom = dom.parentElement;
  312. reactProps = Object.keys(dom.parentElement).find((key) => key.startsWith("__reactProps$"));
  313. }
  314. return dom?.parentElement?.[reactProps]?.children[0]?._owner?.stateNode;
  315. }
  316.  
  317. function startListeningLesson() {
  318. const listeningButton = document.querySelector('button.V59Mk._1tbaI span._1fHYG');
  319. if (listeningButton && listeningButton.textContent.includes('Escuta')) {
  320. listeningButton.click();
  321. }
  322. }
  323.  
  324. setInterval(() => {
  325. addButtons();
  326. startListeningLesson();
  327. }, 3000);
  328.  
  329. window.findReact = findReact;
  330. window.ss = solving;