NopeCHA - Automated reCAPTCHA Solver

AI for Automatic reCAPTCHA Recognition

  1. // ==UserScript==
  2. // @name NopeCHA - Automated reCAPTCHA Solver
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.4
  5. // @description AI for Automatic reCAPTCHA Recognition
  6. // @author You
  7. // @require https://update.greasyfork.org/scripts/534380/1580503/UserscriptSettings.js
  8. // @match https://www.google.com/recaptcha/api2/bframe*
  9. // @match https://www.google.com/recaptcha/api2/anchor*
  10. // @icon https://nopecha.com/apple-icon-72x72.png
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @connect api.nopecha.com
  16. // @license MIT
  17. // ==/UserScript==
  18. const HOST = document.referrer ? document.referrer.split("/")[2] : location.origin;
  19. const API_ENDPOINT = "https://api.nopecha.com";
  20. const GRID_SIZES = { 1: 1, 0: 3, 2: 4 };
  21. const POLL_TIMEOUT = 60000;
  22. const MAX_ATTEMPTS = 30;
  23. const settings = UserscriptSettings;
  24. settings.define({
  25. key: {
  26. name: "Enter your key",
  27. default: "",
  28. title: "",
  29. },
  30. show_data: {
  31. name: "Check API Quota",
  32. default: "",
  33. title: "Click to view your current API usage and reset time",
  34. onclick: () => {
  35. apiRequest(`${API_ENDPOINT}/status`).then(data => {
  36. const time = `${Math.floor(data.ttl/3600)}h ${Math.floor((data.ttl%3600)/60)}m`;
  37. console.log(`Your free quota will reset in ${time}.`);
  38. alert(JSON.stringify(data, null, 2));
  39. });
  40. }
  41. },
  42. disabled_hosts: {
  43. name: (current) => `${current.includes(HOST) ? 'Enable' : 'Disable'} this site`,
  44. default: [],
  45. title: "Add this site to the list of disabled hosts.",
  46. onclick: (current, update) => {
  47. if (current.includes(HOST)) {
  48. current = current.filter(item => item !== HOST);
  49. } else {
  50. current.push(HOST);
  51. }
  52. update(current);
  53. console.log(current);
  54. },
  55. },
  56. solve_delay_time: {
  57. name: "Delay Solving",
  58. default: 2000,
  59. title: "Milliseconds to delay solving.",
  60. },
  61. auto_open: {
  62. name: "Auto-Open",
  63. default: true,
  64. title: "Automatically opens CAPTCHA challenges.",
  65. },
  66. auto_solve: {
  67. name: "Auto-Solve",
  68. default: true,
  69. title: "Automatically solves CAPTCHA challenges.",
  70. },
  71. solve_delay: {
  72. name: "Delay Solving",
  73. default: true,
  74. title: "Adds a delay to avoid detection.",
  75. },
  76. });
  77. const eventQueue = [], eventHandlers = [];
  78. let checkboxObserver, intersectionObserver, captchaObserver,
  79. isRecaptchaActive = false, isCaptchaActive = false;
  80. async function solveCaptcha(params) {
  81. for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
  82. try {
  83. const response = await apiRequest(API_ENDPOINT, {
  84. method: 'POST',
  85. data: { ...params, type: 'recaptcha' }
  86. });
  87. if (!response.error) return pollCaptchaResult(response.data);
  88. if ([10, 11, 12, 15, 16, 17].includes(response.error)) {
  89. await delay(1000);
  90. continue;
  91. }
  92. throw new Error(response.message || `Error ${response.error}`);
  93. } catch (error) {
  94. if (attempt === MAX_ATTEMPTS-1) throw error;
  95. await delay(1000);
  96. }
  97. }
  98. }
  99. async function pollCaptchaResult(recognitionId) {
  100. const startTime = Date.now();
  101. while (Date.now() - startTime < POLL_TIMEOUT) {
  102. const response = await apiRequest(`${API_ENDPOINT}/?id=${recognitionId}`);
  103. if (!response.error) return response;
  104. await delay(1000);
  105. }
  106. throw new Error('Polling timeout');
  107. }
  108. async function apiRequest(url, options = {}) {
  109. const headers = {
  110. 'Accept': 'application/json',
  111. 'Content-Type': 'application/json',
  112. };
  113. if (settings.get("key")) {
  114. headers.Authorization = `Bearer ${settings.get("key")}`;
  115. }
  116. return new Promise((resolve, reject) => {
  117. GM_xmlhttpRequest({
  118. url,
  119. method: options.method || 'GET',
  120. headers,
  121. data: options.data ? JSON.stringify(options.data) : null,
  122. responseType: 'json',
  123. onload: response => resolve(response.response),
  124. onerror: reject,
  125. ontimeout: reject,
  126. onabort: reject
  127. });
  128. });
  129. }
  130. async function loadImage(image, target, timeout = 10000) {
  131. if (!target && !image.complete && !await new Promise(resolve => {
  132. const timer = setTimeout(() => resolve(false), timeout);
  133. image.addEventListener("load", () => {
  134. clearTimeout(timer);
  135. resolve(true);
  136. });
  137. })) return;
  138. const canvas = createCanvas(
  139. image.naturalWidth || target?.clientWidth,
  140. image.naturalHeight || target?.clientHeight
  141. );
  142. canvas.getContext("2d", { willReadFrequently: true }).drawImage(image, 0, 0);
  143. return !isCanvasEmpty(canvas) && canvas;
  144. }
  145. function getPixelColor(imageData, t, n, o) {
  146. let index = (o * t + n) * 4;
  147. return [imageData[index], imageData[index + 1], imageData[index + 2]]
  148. }
  149. function isImageEmpty(canvas, minThreshold = 0, maxThreshold = 230, emptyRatio = 0.99) {
  150. const context = canvas.getContext("2d", { willReadFrequently: true });
  151. const width = context.canvas.width;
  152. const height = context.canvas.height;
  153. if (width === 0 || height === 0) return true;
  154. const imageData = context.getImageData(0, 0, width, height).data;
  155. let emptyPixels = 0;
  156. for (let y = 0; y < height; y++) {
  157. for (let x = 0; x < width; x++) {
  158. const color = getPixelColor(imageData, width, x, y, 1);
  159. const isColorBelowThreshold = color.every(value => value <= minThreshold);
  160. const isColorAboveThreshold = color.every(value => value >= maxThreshold);
  161. if (isColorBelowThreshold || isColorAboveThreshold) emptyPixels++;
  162. }
  163. }
  164. return emptyPixels / (width * height) > emptyRatio;
  165. }
  166. let isSolving = false;
  167. async function startCaptchaSolving() {
  168. if(isSolving) return;
  169. isSolving = true;
  170. while (isCaptchaActive && (getCaptchaHeader() || isVerifyButtonDisabled())) {
  171. await delay(1000);
  172. }
  173. while(isCaptchaActive) {
  174. let { task, type, cells, images, waitAfterSolve } = await getCaptchaInfo();
  175. let startTime = new Date().valueOf(), processedCells = [...cells];
  176. type !== 1 && (images = [images[0]]);
  177. let processedImages = await Promise.all(images.map(s => loadImage(s)));
  178. if(type === 1) {
  179. const validCells = [], validImages = [];
  180. for(const [index, img] of processedImages.entries()) {
  181. img.width !== 100 || img.height !== 100 || (validCells.push(processedCells[index]), validImages.push(img));
  182. }
  183. processedCells = validCells;
  184. processedImages = validImages;
  185. }
  186.  
  187. if(processedImages.length === 0) {
  188. clickElement("#recaptcha-verify-button");
  189. await delay(3000);
  190. continue;
  191. }
  192. if(processedImages.some(isImageEmpty)) {
  193. await delay(3000);
  194. continue;
  195. }
  196. const gridSize = GRID_SIZES[type];
  197. const response = await solveCaptcha({
  198. task,
  199. grid: `${gridSize}x${gridSize}`,
  200. image_data: processedImages.map(canvasToBase64),
  201. })
  202. if(!response || "error" in response) {
  203. console.warn("api error", response), await delay(2e3);
  204. continue
  205. }
  206. const endTime = new Date().valueOf();
  207. if(settings.get("solve_delay")) {
  208. const delayTime = settings.get("solve_delay_time") - endTime + startTime;
  209. delayTime > 0 && await delay(delayTime)
  210. }
  211. const gridWidth = type === 2 ? 4 : 3;
  212. for(processedCells.forEach((s, x) => {
  213. let B = s.classList.contains("rc-imageselect-tileselected"),
  214. h = cells.indexOf(s);
  215. response.data[x] !== B && clickElement(`tr:nth-child(${Math.floor(h/gridWidth)+1}) td:nth-child(${h%gridWidth+1})`)
  216. }),
  217. (!waitAfterSolve || !response.data.some(s => s)) && (await delay(200), clickElement("#recaptcha-verify-button")), await waitForEvent(eventQueue); document.querySelectorAll(".rc-imageselect-dynamic-selected").length > 0;) await delay(1e3)
  218. }
  219. }
  220. if (location.pathname.endsWith("/anchor")) {
  221. settings.createMenu();
  222. registerEventHandler({
  223. name: "auto-open",
  224. condition: () => settings.get("auto_open") && !settings.get("disabled_hosts").includes(HOST),
  225. ready: () => document.contains(document.querySelector(".recaptcha-checkbox")),
  226. start: initializeRecaptcha,
  227. quit: () => {
  228. checkboxObserver.disconnect();
  229. intersectionObserver.disconnect();
  230. isRecaptchaActive = false;
  231. },
  232. running: () => isRecaptchaActive
  233. })
  234. } else {
  235. registerEventHandler({
  236. name: "auto-solve",
  237. condition: () => settings.get("auto_solve") && !settings.get("disabled_hosts").includes(HOST),
  238. ready: () => document.contains(document.querySelector(".rc-imageselect, .rc-imageselect-target")),
  239. start: initializeCaptcha,
  240. quit: () => {
  241. captchaObserver.disconnect();
  242. isCaptchaActive = false;
  243. processEvents(eventQueue)
  244. },
  245. running: () => isCaptchaActive
  246. })
  247. }
  248. async function checkEventHandler(handler) {
  249. if (handler.timedout) return false;
  250. const condition = handler.condition();
  251. if (condition === handler.running()) return false;
  252. if (!condition && handler.running()) {
  253. handler.quit();
  254. return false;
  255. }
  256. if (condition && !handler.running()) {
  257. while (!handler.ready()) await delay(200);
  258. handler.start();
  259. return false;
  260. }
  261. }
  262. function createCanvas(width, height) {
  263. const canvas = document.createElement("canvas");
  264. canvas.width = width;
  265. canvas.height = height;
  266. return canvas;
  267. }
  268. function canvasToBase64(canvas) {
  269. return canvas.toDataURL("image/jpeg").replace(/data:image\/[a-z]+;base64,/g, "");
  270. }
  271. function isCanvasEmpty(canvas) {
  272. try {
  273. canvas.getContext("2d", { willReadFrequently: true }).getImageData(0, 0, 1, 1);
  274. } catch {
  275. return true;
  276. }
  277. return false;
  278. }
  279. function delay(milliseconds) {
  280. return new Promise(resolve => setTimeout(resolve, milliseconds));
  281. }
  282. function initializeCaptcha() {
  283. isCaptchaActive = true;
  284. processEvents(eventQueue);
  285. let captchaTimeout;
  286. captchaObserver = new MutationObserver(() => {
  287. clearTimeout(captchaTimeout);
  288. captchaTimeout = setTimeout(() => processEvents(eventQueue), 200);
  289. });
  290. captchaObserver.observe(document.body, { childList: true, subtree: true });
  291. startCaptchaSolving();
  292. }
  293. function processEvents(queue) {
  294. console.log(queue);
  295. queue.forEach(callback => callback());
  296. queue.splice(0);
  297. }
  298. function registerEventHandler(handler, timeoutDuration) {
  299. handler.timedout = false;
  300. eventHandlers.push(handler);
  301. let timeout;
  302. let interval = setInterval(async () => {
  303. await checkEventHandler(handler) || (clearTimeout(timeout), clearInterval(interval));
  304. }, 400);
  305. timeoutDuration && (timeout = setTimeout(() => clearInterval(interval), timeoutDuration), handler.timedout = true);
  306. }
  307. function waitForEvent(queue) {
  308. return new Promise(resolve => queue.push(resolve));
  309. }
  310. function initializeRecaptcha() {
  311. isRecaptchaActive = true;
  312. checkboxObserver = new MutationObserver(changes => {
  313. if (changes.length === 2) {
  314. handleCheckboxChange();
  315. }
  316. if (changes.length && changes[0].target.classList.contains("recaptcha-checkbox-expired")) {
  317. location.reload();
  318. }
  319. });
  320. checkboxObserver.observe(document.querySelector(".recaptcha-checkbox"), {
  321. attributes: true
  322. });
  323. let isIntersected = false;
  324. intersectionObserver = new IntersectionObserver(() => {
  325. if (!isIntersected) {
  326. isIntersected = true;
  327. handleCheckboxChange();
  328. }
  329. }, {
  330. threshold: 0
  331. });
  332. intersectionObserver.observe(document.body);
  333. }
  334. function isVerifyButtonDisabled() {
  335. return document.querySelector("#recaptcha-verify-button")?.getAttribute("disabled");
  336. }
  337. async function handleCheckboxChange() {
  338. await delay(400);
  339. clickElement(".recaptcha-checkbox");
  340. }
  341. function clickElement(selector) {
  342. document.querySelector(selector)?.click();
  343. }
  344. function getCaptchaHeader() {
  345. return document.querySelector(".rc-doscaptcha-header");
  346. }
  347. function getCaptchaInfo() {
  348. return new Promise(resolve => {
  349. const interval = setInterval(() => {
  350. const instructions = document.querySelector(".rc-imageselect-instructions");
  351. const cells = [...document.querySelectorAll("table tr td")];
  352. const images = cells.map(cell => cell.querySelector("img")); //.filter(c => c).filter(c => c.src.trim());
  353. if (!instructions || cells.concat(images).length < 18) return;
  354. clearInterval(interval);
  355. const lines = instructions.innerText.split("\n");
  356. const task = lines.slice(0, 2).join(" ").replace(/\s+/g, " ").trim();
  357. const type = cells.length === 16 ? 2 : images.some(img => img.classList.contains("rc-image-tile-11")) ? 1 : 0;
  358. const waitAfterSolve = lines.length === 3 && type !== 2;
  359. resolve({ task, type, cells, images, waitAfterSolve });
  360. }, 1000);
  361. });
  362. }