using 2captcha api instead of manually solving with tesseract. ke.y is in script but i dont care

This commit is contained in:
2026-02-15 12:11:31 +01:00
parent ddb712ba4a
commit 4ed259ca5a
15 changed files with 419 additions and 72 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

BIN
View File
Binary file not shown.
+7
View File
@@ -6,3 +6,10 @@ DATE | TIME | WEBSITE | REASON
14/02/2026 | 22:46:37 | Gate6 (https://gate6.vn/) | Navigating frame was detached
14/02/2026 | 22:46:37 | Kulala Land (https://kulalaland. | Navigating frame was detached
14/02/2026 | 22:46:37 | sneakywholefoods (https://www.sn | Connection closed.
14/02/2026 | 23:00:22 | Site_1 (https://jubina.com/list) | Failure keyword detected: "verification"
14/02/2026 | 23:04:00 | Site_1 (https://jubina.com/list) | Submit failed: Execution context was destroyed, most likely because of a navigation.
15/02/2026 | 00:06:51 | Site_1 (https://jubina.com/list) | Form still visible after submission (likely failed)
15/02/2026 | 00:18:09 | Dutch News Test (https://www.dut | Form still visible after submission (likely failed)
15/02/2026 | 00:18:27 | Site_1 (https://jubina.com/list) | Form still visible after submission (likely failed)
15/02/2026 | 00:18:50 | Site_1 (https://jubina.com/list) | Form still visible after submission (likely failed)
15/02/2026 | 00:20:19 | Site_1 (https://jubina.com/list) | Form still visible after submission (likely failed)
+1 -1
View File
@@ -84,7 +84,7 @@ async function main() {
const site = sitesToProcess[i];
console.log(`[${i + 1}/${sitesToProcess.length}]`);
const result = await processSite(browser, site, globalCreds, browserConfig, cliOptions.keepOpen);
const result = await processSite(browser, site, globalCreds, browserConfig, cliOptions.keepOpen, cliOptions.dryRun);
if (result.siteFailed) {
failedSites.push({
+1 -1
View File
@@ -1,6 +1,6 @@
https://immaculatevegan.com/blogs/magazine
https://gate6.vn/
https://www.sprintzeal.com/blog
https://www.sprintzeal.com/
https://kulalaland.com/
https://www.goplaces.co.uk/
https://www.fiftyfiftybottles.com/
+20
View File
@@ -10,9 +10,11 @@
"license": "ISC",
"dependencies": {
"@iarna/toml": "^2.2.5",
"2captcha": "^3.0.5-2",
"commander": "^14.0.3",
"playwright": "^1.56.1",
"playwright-extra": "^4.3.6",
"pngjs": "^7.0.0",
"puppeteer": "^24.30.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
@@ -110,6 +112,15 @@
"@types/node": "*"
}
},
"node_modules/2captcha": {
"version": "3.0.5-2",
"resolved": "https://registry.npmjs.org/2captcha/-/2captcha-3.0.5-2.tgz",
"integrity": "sha512-p/C1UFss2PvBxYjypuRZ4zE1n3C5VK7hnNijnTetF9sKg5iifF6xfos3h3CJQLGB//0Ksa1C77ItM3gOhHy7fA==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.1"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1186,6 +1197,15 @@
}
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+2
View File
@@ -12,9 +12,11 @@
"type": "commonjs",
"dependencies": {
"@iarna/toml": "^2.2.5",
"2captcha": "^3.0.5-2",
"commander": "^14.0.3",
"playwright": "^1.56.1",
"playwright-extra": "^4.3.6",
"pngjs": "^7.0.0",
"puppeteer": "^24.30.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
+172 -21
View File
@@ -1,4 +1,9 @@
const Tesseract = require('tesseract.js');
const https = require('https');
const http = require('http');
const TwoCaptcha = require('2captcha');
const solver = new TwoCaptcha.Solver('9ed0ef51badf9a017ac50aea413d8001');
const CAPTCHA_PATTERNS = {
recaptcha: {
@@ -23,6 +28,18 @@ const CAPTCHA_PATTERNS = {
}
};
function fetchImageAsBuffer(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
client.get(url, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => resolve(Buffer.concat(chunks)));
res.on('error', reject);
}).on('error', reject);
});
}
async function detectCaptcha(page) {
const captchaInfo = await page.evaluate((patterns) => {
const results = [];
@@ -103,30 +120,163 @@ async function solveSimpleCaptcha(page) {
});
if (imageCaptchaInfo) {
console.log(' Attempting OCR-based CAPTCHA solving...');
try {
const { data: { text } } = await Tesseract.recognize(
imageCaptchaInfo.src,
'eng',
{ logger: m => {} }
);
console.log(' Attempting 2Captcha image solving...');
console.log(` CAPTCHA image selector: ${imageCaptchaInfo.selector}`);
// Wait for CAPTCHA image to fully load before capturing
console.log(' Waiting for CAPTCHA image to load...');
let imageLoaded = false;
const maxWaitMs = 5000;
const checkIntervalMs = 200;
let waitedMs = 0;
while (!imageLoaded && waitedMs < maxWaitMs) {
imageLoaded = await page.evaluate((selector) => {
const el = document.querySelector(selector);
return el && el.naturalWidth > 0 && el.naturalHeight > 0;
}, imageCaptchaInfo.selector).catch(() => false);
const cleanedText = text.replace(/[^a-zA-Z0-9]/g, '').trim();
if (cleanedText && cleanedText.length >= 3) {
console.log(` OCR read: "${cleanedText}"`);
return {
type: 'image',
answer: cleanedText,
originalText: text,
imageSrc: imageCaptchaInfo.src
};
if (!imageLoaded) {
await new Promise(r => setTimeout(r, checkIntervalMs));
waitedMs += checkIntervalMs;
}
} catch (ocrError) {
console.log(` OCR failed: ${ocrError.message}`);
}
return { type: 'image', message: 'Image CAPTCHA detected - OCR failed, manual solving required' };
if (imageLoaded) {
console.log(` CAPTCHA image loaded successfully (waited ${waitedMs}ms)`);
} else {
console.log(' WARNING: CAPTCHA may not have fully loaded, proceeding anyway...');
}
// Additional small delay to ensure rendering is complete
await new Promise(r => setTimeout(r, 300));
try {
let imageData;
// Get CAPTCHA element bounding box
const captchaInfo = await page.evaluate((selector) => {
const img = document.querySelector(selector);
if (!img) return null;
const rect = img.getBoundingClientRect();
return {
src: img.src,
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
};
}, imageCaptchaInfo.selector);
if (captchaInfo && captchaInfo.width > 0) {
console.log(' CAPTCHA at:', captchaInfo.x, captchaInfo.y, captchaInfo.width, 'x', captchaInfo.height);
// Try to get element screenshot directly
try {
const captchaElement = await page.$(imageCaptchaInfo.selector);
if (captchaElement) {
const elementScreenshot = await captchaElement.screenshot({ type: 'png' });
if (Buffer.isBuffer(elementScreenshot)) {
imageData = elementScreenshot.toString('base64');
} else {
imageData = Buffer.from(elementScreenshot).toString('base64');
}
console.log(' Element screenshot captured, size:', imageData.length, 'bytes (base64)');
}
} catch (elemError) {
console.log(' Element screenshot failed:', elemError.message);
// Fallback: capture full page and crop
const fullScreenshot = await page.screenshot({ type: 'png', encoding: 'binary' });
const screenshotBuffer = Buffer.from(fullScreenshot, 'binary');
const png = require('pngjs').PNG.sync.read(screenshotBuffer);
const cropX = Math.max(0, Math.floor(captchaInfo.x));
const cropY = Math.max(0, Math.floor(captchaInfo.y));
const cropW = Math.floor(captchaInfo.width);
const cropH = Math.floor(captchaInfo.height);
console.log(' Cropping to:', cropX, cropY, cropW, cropH);
const cropped = new require('pngjs').PNG({ width: cropW, height: cropH });
for (let y = 0; y < cropH; y++) {
for (let x = 0; x < cropW; x++) {
const srcIdx = ((cropY + y) * png.width + (cropX + x)) * 4;
const dstIdx = (y * cropW + x) * 4;
cropped.data[dstIdx] = png.data[srcIdx];
cropped.data[dstIdx + 1] = png.data[srcIdx + 1];
cropped.data[dstIdx + 2] = png.data[srcIdx + 2];
cropped.data[dstIdx + 3] = png.data[srcIdx + 3];
}
}
imageData = require('pngjs').PNG.sync.write(cropped).toString('base64');
console.log(' Cropped image size:', imageData.length, 'bytes (base64)');
}
} else if (captchaInfo && imageCaptchaInfo.src.startsWith('data:')) {
const base64 = imageCaptchaInfo.src.replace(/^data:image\/\w+;base64,/, '');
imageData = base64;
}
console.log(' Sending to 2Captcha (human workers)...');
const result = await solver.imageCaptcha(imageData, {
numeric: 0,
minLength: 1,
maxLength: 10,
phrase: 0,
caseSensitive: 1,
calc: 0,
lang: 'en'
});
console.log(` 2Captcha solved: "${result.data}"`);
return {
type: 'image',
answer: result.data.trim(),
originalText: result.data,
service: '2captcha',
imageSrc: imageCaptchaInfo.src
};
} catch (error) {
console.log(` 2Captcha failed: ${error.message}`);
console.log(' Falling back to Tesseract OCR...');
try {
const { data: { text, confidence } } = await Tesseract.recognize(
imageCaptchaInfo.src,
'eng',
{ logger: m => {} }
);
console.log(` OCR raw output: "${text}"`);
console.log(` OCR confidence: ${confidence}%`);
const cleanedText = text.replace(/[^a-zA-Z0-9]/g, '').trim();
if (cleanedText && cleanedText.length >= 3) {
console.log(` OCR fallback read: "${cleanedText}"`);
return {
type: 'image',
answer: cleanedText,
originalText: text,
confidence: confidence,
service: 'tesseract-fallback',
imageSrc: imageCaptchaInfo.src
};
}
} catch (ocrError) {
console.log(` OCR fallback also failed: ${ocrError.message}`);
}
return {
type: 'image',
answer: null,
message: `Both 2Captcha and OCR failed: ${error.message}`,
imageSrc: imageCaptchaInfo.src
};
}
}
return null;
@@ -135,5 +285,6 @@ async function solveSimpleCaptcha(page) {
module.exports = {
CAPTCHA_PATTERNS,
detectCaptcha,
solveSimpleCaptcha
solveSimpleCaptcha,
fetchImageAsBuffer
};
+24 -20
View File
@@ -10,36 +10,39 @@ function createCLI() {
.option('-f, --file <path>', 'Load sites from text file (one URL per line)')
.option('-k, --keep-open', 'Keep browser tabs open after processing for verification')
.option('-l, --log', 'Save output to timestamped log file')
.option('-d, --dry-run', 'Test mode - detect CAPTCHA but skip solving (no API calls)')
.option('-h, --help', 'Show help message')
.argument('[target]', 'Site name or URL to process (filters from config.toml sites)');
program.on('option:help', () => {
console.log(`
Newsletter Signup Automation Tool
Newsletter Signup Automation Tool
Usage: node index.js [options] [site-name-or-url]
Usage: node index.js [options] [site-name-or-url]
Options:
-f, --file=<path> Load sites from text file (one URL per line)
-k, --keep-open Keep browser tabs open after processing for verification
-l, --log Save output to timestamped log file
-h, --help Show this help message
Options:
-f, --file=<path> Load sites from text file (one URL per line)
-k, --keep-open Keep browser tabs open after processing for verification
-l, --log Save output to timestamped log file
-d, --dry-run Test mode - detect CAPTCHA but skip solving (no API calls)
-h, --help Show this help message
Examples:
node index.js Process all sites from config.toml
node index.js --file=sites.txt Process sites from file
node index.js "Dutch News" Process specific site
node index.js --file=sites.txt --keep-open Keep tabs open for verification
node index.js --log Save output to log file
Examples:
node index.js Process all sites from config.toml
node index.js --file=sites.txt Process sites from file
node index.js "Dutch News" Process specific site
node index.js --file=sites.txt --keep-open Keep tabs open for verification
node index.js --log Save output to log file
node index.js --dry-run Test without calling 2Captcha API
Text file format (sites.txt):
# Lines starting with # are comments
https://example.com/newsletter
https://another-site.com/signup
Text file format (sites.txt):
# Lines starting with # are comments
https://example.com/newsletter
https://another-site.com/signup
Configuration:
Edit config.toml to set credentials and default sites
`);
Configuration:
Edit config.toml to set credentials and default sites
`);
process.exit(0);
});
@@ -59,6 +62,7 @@ function parseCLI(args = process.argv.slice(2)) {
file: opts.file || null,
keepOpen: opts.keepOpen || false,
log: opts.log || false,
dryRun: opts.dryRun || false,
help: opts.help || false
};
}
+93 -29
View File
@@ -3,8 +3,9 @@ const { detectFormFields } = require('./fields');
const { detectSubmitButton } = require('./submit');
const { detectCaptcha, solveSimpleCaptcha } = require('./captcha');
const { classifyNewsletterForm } = require('./classifier');
const { detectSubmissionResult } = require('./submission');
async function processSite(browser, site, globalCreds, browserConfig, keepOpen = false) {
async function processSite(browser, site, globalCreds, browserConfig, keepOpen = false, dryRun = false) {
let siteFailed = false;
let failureReason = '';
let page = null;
@@ -42,38 +43,66 @@ async function processSite(browser, site, globalCreds, browserConfig, keepOpen =
}
if (captchaInfo.some(c => c.type === 'mathCaptcha')) {
const solution = await solveSimpleCaptcha(page);
if (solution && solution.answer) {
console.log(` Auto-solving math: ${solution.text} = ${solution.answer}`);
try {
await page.evaluate((answer) => {
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], #captcha');
if (input) {
input.value = answer;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}, solution.answer);
} catch (e) {
console.warn(` Failed to fill math answer: ${e.message}`);
if (dryRun) {
console.log(` [DRY-RUN] Would solve math CAPTCHA`);
} else {
const solution = await solveSimpleCaptcha(page);
if (solution && solution.answer) {
console.log(` Auto-solving math: ${solution.text} = ${solution.answer}`);
try {
await page.evaluate((answer) => {
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], #captcha');
if (input) {
input.value = answer;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}, solution.answer);
} catch (e) {
console.warn(` Failed to fill math answer: ${e.message}`);
}
}
}
}
if (captchaInfo.some(c => c.type === 'imageCaptcha')) {
const solution = await solveSimpleCaptcha(page);
if (solution && solution.type === 'image' && solution.answer) {
console.log(` OCR solved image CAPTCHA: "${solution.answer}"`);
try {
await page.evaluate((answer) => {
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], input[id*="captcha"], #captcha, .captcha-input');
if (input) {
input.value = answer;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}, solution.answer);
} catch (e) {
console.warn(` Failed to fill OCR answer: ${e.message}`);
if (captchaInfo.some(c => c.type === 'textCaptcha')) {
console.log(` Text CAPTCHA detected - looking for input field...`);
const textCaptchaFilled = await page.evaluate(() => {
const input = document.querySelector('input[name*="captcha"], input[id*="captcha"], .captcha-input, #captcha, input[placeholder*="captcha" i]');
if (input) {
return {
found: true,
selector: input.id ? `#${input.id}` : (input.name ? `[name="${input.name}"]` : 'input[name*="captcha"]')
};
}
return { found: false };
});
if (textCaptchaFilled.found) {
console.log(` Text CAPTCHA input found: ${textCaptchaFilled.selector}`);
}
}
if (captchaInfo.some(c => c.type === 'imageCaptcha' || c.type === 'textCaptcha')) {
if (dryRun) {
console.log(` [DRY-RUN] Would solve image/text CAPTCHA (no API calls)`);
} else {
const solution = await solveSimpleCaptcha(page);
if (solution && solution.answer) {
console.log(` OCR solved CAPTCHA: "${solution.answer}"`);
try {
await page.evaluate((answer) => {
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], input[id*="captcha"], #captcha, .captcha-input');
if (input) {
input.value = answer;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
console.log(` Filled CAPTCHA with: ${answer}`);
}
}, solution.answer);
} catch (e) {
console.warn(` Failed to fill CAPTCHA answer: ${e.message}`);
}
} else if (solution && solution.message) {
console.log(` CAPTCHA solving failed: ${solution.message}`);
}
}
}
@@ -190,8 +219,43 @@ async function processSite(browser, site, globalCreds, browserConfig, keepOpen =
if (submitSelector) {
try {
const urlBeforeSubmit = page.url();
await page.click(submitSelector);
console.log(` Submitted (${submitSelector})`);
await new Promise(r => setTimeout(r, 1500));
let submissionResult;
try {
const urlAfterSubmit = page.url();
if (urlAfterSubmit !== urlBeforeSubmit) {
console.log(` Page navigated to: ${urlAfterSubmit}`);
}
submissionResult = await detectSubmissionResult(page);
} catch (resultError) {
console.log(` Could not detect submission result: ${resultError.message}`);
submissionResult = { failure: false, success: false, signals: ['Could not determine - page may have redirected'] };
}
console.log(`\n Submission Result Analysis:`);
if (submissionResult.signals && submissionResult.signals.length > 0) {
for (const signal of submissionResult.signals) {
console.log(` - ${signal}`);
}
}
if (submissionResult.failure) {
siteFailed = true;
failureReason = failureReason || submissionResult.failureReason || 'Form submission failed';
console.error(` FAILED: ${submissionResult.failureReason}`);
} else if (submissionResult.success) {
console.log(` SUCCESS: Subscription appears to have succeeded`);
} else {
console.log(` Result: Unknown (page may have redirected to confirmation)`);
}
} catch (submitError) {
siteFailed = true;
failureReason = failureReason || `Submit failed: ${submitError.message}`;
+97
View File
@@ -0,0 +1,97 @@
async function detectSubmissionResult(page) {
const result = await page.evaluate(() => {
const results = {
success: false,
failure: false,
failureReason: null,
currentUrl: window.location.href,
signals: []
};
const errorSelectors = [
'.error', '.Error', '.ERROR',
'[class*="error" i]',
'.alert-danger', '.alert-error',
'[class*="failed" i]',
'[class*="invalid" i]',
'[class*="wrong" i]',
'#error', '#Error',
'[role="alert"]',
'.message.error',
'.error-message',
'.form-error',
'[data-form-error]'
];
for (const selector of errorSelectors) {
const el = document.querySelector(selector);
if (el) {
const text = el.textContent?.toLowerCase() || '';
const style = window.getComputedStyle(el);
if (style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
text.length > 0 &&
text.length < 500) {
results.failure = true;
results.failureReason = `Error message detected: ${text.substring(0, 100)}`;
results.signals.push(`Error: ${text.substring(0, 100)}`);
break;
}
}
}
const pageText = document.body.innerText.toLowerCase();
const successKeywords = [
'thank you', 'thanks for', 'successfully', 'subscribed', 'subscription confirmed',
'you are now', 'welcome', 'email confirmed', 'verification sent',
'check your email', 'confirm your email', 'almost done',
'signed up', 'signed up successfully', 'subscription successful'
];
for (const keyword of successKeywords) {
if (pageText.includes(keyword)) {
results.success = true;
results.signals.push(`Success keyword: "${keyword}"`);
break;
}
}
const failureKeywords = [
'incorrect', 'invalid captcha', 'wrong captcha', 'incorrect captcha',
'captcha incorrect', 'captcha invalid', 'captcha error',
'please complete the captcha', 'complete the captcha',
'try again', 'submission failed'
];
if (!results.failure) {
for (const keyword of failureKeywords) {
if (pageText.includes(keyword)) {
results.failure = true;
results.failureReason = `Failure keyword detected: "${keyword}"`;
results.signals.push(`Failure: ${keyword}`);
break;
}
}
}
const formStillVisible = document.querySelector('form') !== null;
const emailInputStillVisible = document.querySelector('input[type="email"]') !== null ||
document.querySelector('input[name*="email" i]') !== null;
if (formStillVisible && emailInputStillVisible && !results.success) {
results.failure = true;
results.failureReason = results.failureReason || 'Form still visible after submission (likely failed)';
results.signals.push('Form still visible - submission may have failed');
}
return results;
});
return result;
}
module.exports = {
detectSubmissionResult
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

+1
View File
@@ -0,0 +1 @@
https://jubina.com/list
+1
View File
@@ -0,0 +1 @@
https://jubina.com/list
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B