changed wait for hCaptcha image logic & other stuff
- fixed bug in dragging type of hCaptcha when worker did not select an even amount of coordinates and it would crash - change waitForResponse function to a waitForRequests util function with more proper checks
This commit is contained in:
		
							parent
							
								
									52ad4dea00
								
							
						
					
					
						commit
						881c6c773c
					
				@ -1,7 +1,7 @@
 | 
				
			|||||||
# For more information, please see the README.md
 | 
					# For more information, please see the README.md
 | 
				
			||||||
SUNO_COOKIE=
 | 
					SUNO_COOKIE=
 | 
				
			||||||
TWOCAPTCHA_KEY= # Obtain from 2captcha.com
 | 
					TWOCAPTCHA_KEY= # Obtain from 2captcha.com
 | 
				
			||||||
BROWSER=chromium # chromium or firefox
 | 
					BROWSER=chromium # chromium or firefox, although chromium is highly recommended
 | 
				
			||||||
BROWSER_GHOST_CURSOR=false
 | 
					BROWSER_GHOST_CURSOR=false
 | 
				
			||||||
BROWSER_LOCALE=en
 | 
					BROWSER_LOCALE=en
 | 
				
			||||||
BROWSER_HEADLESS=true
 | 
					BROWSER_HEADLESS=true
 | 
				
			||||||
@ -2,13 +2,13 @@ import axios, { AxiosInstance } from 'axios';
 | 
				
			|||||||
import UserAgent from 'user-agents';
 | 
					import UserAgent from 'user-agents';
 | 
				
			||||||
import pino from 'pino';
 | 
					import pino from 'pino';
 | 
				
			||||||
import yn from 'yn';
 | 
					import yn from 'yn';
 | 
				
			||||||
import { sleep, isPage } from '@/lib/utils';
 | 
					import { isPage, sleep, waitForRequests } from '@/lib/utils';
 | 
				
			||||||
import * as cookie from 'cookie';
 | 
					import * as cookie from 'cookie';
 | 
				
			||||||
import { randomUUID } from 'node:crypto';
 | 
					import { randomUUID } from 'node:crypto';
 | 
				
			||||||
import { Solver } from '@2captcha/captcha-solver';
 | 
					import { Solver } from '@2captcha/captcha-solver';
 | 
				
			||||||
 | 
					import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
 | 
				
			||||||
import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
 | 
					import { BrowserContext, Page, Locator, chromium, firefox } from 'rebrowser-playwright-core';
 | 
				
			||||||
import { createCursor, Cursor } from 'ghost-cursor-playwright';
 | 
					import { createCursor, Cursor } from 'ghost-cursor-playwright';
 | 
				
			||||||
import { paramsCoordinates } from '@2captcha/captcha-solver/dist/structs/2captcha';
 | 
					 | 
				
			||||||
import { promises as fs } from 'fs';
 | 
					import { promises as fs } from 'fs';
 | 
				
			||||||
import path from 'node:path';
 | 
					import path from 'node:path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -180,7 +180,6 @@ class SunoApi {
 | 
				
			|||||||
      ctype: 'generation'
 | 
					      ctype: 'generation'
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    logger.info(resp.data);
 | 
					    logger.info(resp.data);
 | 
				
			||||||
    // await sleep(10);
 | 
					 | 
				
			||||||
    return resp.data.required;
 | 
					    return resp.data.required;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -288,40 +287,39 @@ class SunoApi {
 | 
				
			|||||||
      this.cursor = await createCursor(page);
 | 
					      this.cursor = await createCursor(page);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    logger.info('Triggering the CAPTCHA');
 | 
					    logger.info('Triggering the CAPTCHA');
 | 
				
			||||||
    await this.click(page, { x: 318, y: 13 }); // close all popups
 | 
					    try {
 | 
				
			||||||
 | 
					      await page.getByLabel('Close').click({ timeout: 2000 }); // close all popups
 | 
				
			||||||
 | 
					      // await this.click(page, { x: 318, y: 13 });
 | 
				
			||||||
 | 
					    } catch(e) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const textarea = page.locator('.custom-textarea');
 | 
					    const textarea = page.locator('.custom-textarea');
 | 
				
			||||||
    await this.click(textarea);
 | 
					    await this.click(textarea);
 | 
				
			||||||
    await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
 | 
					    await textarea.pressSequentially('Lorem ipsum', { delay: 80 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const button = page.locator('button[aria-label="Create"]').locator('div.flex');
 | 
					    const button = page.locator('button[aria-label="Create"]').locator('div.flex');
 | 
				
			||||||
    await this.click(button);
 | 
					    this.click(button);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    new Promise<void>(async (resolve, reject) => {
 | 
					    new Promise<void>(async (resolve, reject) => {
 | 
				
			||||||
      const frame = page.frameLocator('iframe[title*="hCaptcha"]');
 | 
					      const frame = page.frameLocator('iframe[title*="hCaptcha"]');
 | 
				
			||||||
      const challenge = frame.locator('.challenge-container');
 | 
					      const challenge = frame.locator('.challenge-container');
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
 | 
					        let wait = true;
 | 
				
			||||||
        while (true) {
 | 
					        while (true) {
 | 
				
			||||||
          await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 60000 }); // wait for hCaptcha image to load
 | 
					          if (wait)
 | 
				
			||||||
          while (true) { // wait for all requests to finish
 | 
					            await waitForRequests(page, controller.signal);
 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              await page.waitForResponse('https://img**.hcaptcha.com/**', { timeout: 1000 }); 
 | 
					 | 
				
			||||||
            } catch(e) {
 | 
					 | 
				
			||||||
              break
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
 | 
					          const drag = (await challenge.locator('.prompt-text').first().innerText()).toLowerCase().includes('drag');
 | 
				
			||||||
          let captcha: any;
 | 
					          let captcha: any;
 | 
				
			||||||
          for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could send an error
 | 
					          for (let j = 0; j < 3; j++) { // try several times because sometimes 2Captcha could return an error
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              logger.info('Sending the CAPTCHA to 2Captcha');
 | 
					              logger.info('Sending the CAPTCHA to 2Captcha');
 | 
				
			||||||
              const payload: paramsCoordinates = {
 | 
					              const payload: paramsCoordinates = {
 | 
				
			||||||
                body: (await challenge.screenshot()).toString('base64'),
 | 
					                body: (await challenge.screenshot({ timeout: 5000 })).toString('base64'),
 | 
				
			||||||
                lang: process.env.BROWSER_LOCALE
 | 
					                lang: process.env.BROWSER_LOCALE
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
              if (drag) {
 | 
					              if (drag) {
 | 
				
			||||||
                // Say to the worker that he needs to click
 | 
					                // Say to the worker that he needs to click
 | 
				
			||||||
                payload.textinstructions = '! Instead of dragging, CLICK on the shapes as shown in the image above !';
 | 
					                payload.textinstructions = 'CLICK on the shapes at their edge or center as shown above—please be precise!';
 | 
				
			||||||
                payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
 | 
					                payload.imginstructions = (await fs.readFile(path.join(process.cwd(), 'public', 'drag-instructions.jpg'))).toString('base64');
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              captcha = await this.solver.coordinates(payload);
 | 
					              captcha = await this.solver.coordinates(payload);
 | 
				
			||||||
@ -338,6 +336,12 @@ class SunoApi {
 | 
				
			|||||||
            const challengeBox = await challenge.boundingBox();
 | 
					            const challengeBox = await challenge.boundingBox();
 | 
				
			||||||
            if (challengeBox == null)
 | 
					            if (challengeBox == null)
 | 
				
			||||||
              throw new Error('.challenge-container boundingBox is null!');
 | 
					              throw new Error('.challenge-container boundingBox is null!');
 | 
				
			||||||
 | 
					            if (captcha.data.length % 2) {
 | 
				
			||||||
 | 
					              logger.info('Solution does not have even amount of points required for dragging. Requesting new solution...');
 | 
				
			||||||
 | 
					              this.solver.badReport(captcha.id);
 | 
				
			||||||
 | 
					              wait = false;
 | 
				
			||||||
 | 
					              continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            for (let i = 0; i < captcha.data.length; i += 2) {
 | 
					            for (let i = 0; i < captcha.data.length; i += 2) {
 | 
				
			||||||
              const data1 = captcha.data[i];
 | 
					              const data1 = captcha.data[i];
 | 
				
			||||||
              const data2 = captcha.data[i+1];
 | 
					              const data2 = captcha.data[i+1];
 | 
				
			||||||
@ -348,6 +352,7 @@ class SunoApi {
 | 
				
			|||||||
              await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
 | 
					              await page.mouse.move(challengeBox.x + +data2.x, challengeBox.y + +data2.y, { steps: 30 });
 | 
				
			||||||
              await page.mouse.up();
 | 
					              await page.mouse.up();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            wait = true;
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            for (const data of captcha.data) {
 | 
					            for (const data of captcha.data) {
 | 
				
			||||||
              logger.info(data);
 | 
					              logger.info(data);
 | 
				
			||||||
@ -362,7 +367,8 @@ class SunoApi {
 | 
				
			|||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch(e: any) {
 | 
					      } catch(e: any) {
 | 
				
			||||||
        if (e.message.includes('been closed')) // catch error when closing the browser
 | 
					        if (e.message.includes('been closed') // catch error when closing the browser
 | 
				
			||||||
 | 
					          || e.message == 'AbortError') // catch error when waitForRequests is aborted
 | 
				
			||||||
          resolve();
 | 
					          resolve();
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
          reject(e);
 | 
					          reject(e);
 | 
				
			||||||
@ -377,6 +383,7 @@ class SunoApi {
 | 
				
			|||||||
          logger.info('hCaptcha token received. Closing browser');
 | 
					          logger.info('hCaptcha token received. Closing browser');
 | 
				
			||||||
          route.abort();
 | 
					          route.abort();
 | 
				
			||||||
          browser.browser()?.close();
 | 
					          browser.browser()?.close();
 | 
				
			||||||
 | 
					          controller.abort();
 | 
				
			||||||
          const request = route.request();
 | 
					          const request = route.request();
 | 
				
			||||||
          this.currentToken = request.headers().authorization.split('Bearer ').pop();
 | 
					          this.currentToken = request.headers().authorization.split('Bearer ').pop();
 | 
				
			||||||
          resolve(request.postDataJSON().token);
 | 
					          resolve(request.postDataJSON().token);
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,87 @@ export const isPage = (target: any): target is Page => {
 | 
				
			|||||||
  return target.constructor.name === 'Page';
 | 
					  return target.constructor.name === 'Page';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Waits for an hCaptcha image requests and then waits for all of them to end
 | 
				
			||||||
 | 
					 * @param page
 | 
				
			||||||
 | 
					 * @param signal `const controller = new AbortController(); controller.status`
 | 
				
			||||||
 | 
					 * @returns {Promise<void>} 
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const waitForRequests = (page: Page, signal: AbortSignal): Promise<void> => {
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    const urlPattern = /^https:\/\/img[a-zA-Z0-9]*\.hcaptcha\.com\/.*$/;
 | 
				
			||||||
 | 
					    let timeoutHandle: NodeJS.Timeout | null = null;
 | 
				
			||||||
 | 
					    let activeRequestCount = 0;
 | 
				
			||||||
 | 
					    let requestOccurred = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const cleanupListeners = () => {
 | 
				
			||||||
 | 
					      page.off('request', onRequest);
 | 
				
			||||||
 | 
					      page.off('requestfinished', onRequestFinished);
 | 
				
			||||||
 | 
					      page.off('requestfailed', onRequestFinished);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resetTimeout = () => {
 | 
				
			||||||
 | 
					      if (timeoutHandle)
 | 
				
			||||||
 | 
					        clearTimeout(timeoutHandle);
 | 
				
			||||||
 | 
					      if (activeRequestCount === 0) {
 | 
				
			||||||
 | 
					        timeoutHandle = setTimeout(() => {
 | 
				
			||||||
 | 
					          cleanupListeners();
 | 
				
			||||||
 | 
					          resolve();
 | 
				
			||||||
 | 
					        }, 1000); // 1 second of no requests
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onRequest = (request: { url: () => string }) => {
 | 
				
			||||||
 | 
					      if (urlPattern.test(request.url())) {
 | 
				
			||||||
 | 
					        requestOccurred = true;
 | 
				
			||||||
 | 
					        activeRequestCount++;
 | 
				
			||||||
 | 
					        if (timeoutHandle)
 | 
				
			||||||
 | 
					          clearTimeout(timeoutHandle);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onRequestFinished = (request: { url: () => string }) => {
 | 
				
			||||||
 | 
					      if (urlPattern.test(request.url())) {
 | 
				
			||||||
 | 
					        activeRequestCount--;
 | 
				
			||||||
 | 
					        resetTimeout();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Wait for an hCaptcha request for up to 1 minute
 | 
				
			||||||
 | 
					    const initialTimeout = setTimeout(() => {
 | 
				
			||||||
 | 
					      if (!requestOccurred) {
 | 
				
			||||||
 | 
					        page.off('request', onRequest);
 | 
				
			||||||
 | 
					        cleanupListeners();
 | 
				
			||||||
 | 
					        reject(new Error('No hCaptcha request occurred within 1 minute.'));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Start waiting for no hCaptcha requests
 | 
				
			||||||
 | 
					        resetTimeout();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, 60000); // 1 minute timeout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    page.on('request', onRequest);
 | 
				
			||||||
 | 
					    page.on('requestfinished', onRequestFinished);
 | 
				
			||||||
 | 
					    page.on('requestfailed', onRequestFinished);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Cleanup the initial timeout if an hCaptcha request occurs
 | 
				
			||||||
 | 
					    page.on('request', (request: { url: () => string }) => {
 | 
				
			||||||
 | 
					      if (urlPattern.test(request.url())) {
 | 
				
			||||||
 | 
					        clearTimeout(initialTimeout);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onAbort = () => {
 | 
				
			||||||
 | 
					      cleanupListeners();
 | 
				
			||||||
 | 
					      clearTimeout(initialTimeout);
 | 
				
			||||||
 | 
					      if (timeoutHandle)
 | 
				
			||||||
 | 
					        clearTimeout(timeoutHandle);
 | 
				
			||||||
 | 
					      signal.removeEventListener('abort', onAbort);
 | 
				
			||||||
 | 
					      reject(new Error('AbortError'));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    signal.addEventListener('abort', onAbort, { once: true });
 | 
				
			||||||
 | 
					  }); 
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const corsHeaders = {
 | 
					export const corsHeaders = {
 | 
				
			||||||
  'Access-Control-Allow-Origin': '*',
 | 
					  'Access-Control-Allow-Origin': '*',
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user