using 2captcha api instead of manually solving with tesseract. ke.y is in script but i dont care
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 142 B |
Binary file not shown.
@@ -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 | 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 | Kulala Land (https://kulalaland. | Navigating frame was detached
|
||||||
14/02/2026 | 22:46:37 | sneakywholefoods (https://www.sn | Connection closed.
|
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)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ async function main() {
|
|||||||
const site = sitesToProcess[i];
|
const site = sitesToProcess[i];
|
||||||
console.log(`[${i + 1}/${sitesToProcess.length}]`);
|
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) {
|
if (result.siteFailed) {
|
||||||
failedSites.push({
|
failedSites.push({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
https://immaculatevegan.com/blogs/magazine
|
https://immaculatevegan.com/blogs/magazine
|
||||||
https://gate6.vn/
|
https://gate6.vn/
|
||||||
https://www.sprintzeal.com/blog
|
https://www.sprintzeal.com/
|
||||||
https://kulalaland.com/
|
https://kulalaland.com/
|
||||||
https://www.goplaces.co.uk/
|
https://www.goplaces.co.uk/
|
||||||
https://www.fiftyfiftybottles.com/
|
https://www.fiftyfiftybottles.com/
|
||||||
|
|||||||
Generated
+20
@@ -10,9 +10,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"2captcha": "^3.0.5-2",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"playwright-extra": "^4.3.6",
|
"playwright-extra": "^4.3.6",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"puppeteer": "^24.30.0",
|
"puppeteer": "^24.30.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
@@ -110,6 +112,15 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"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": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"2captcha": "^3.0.5-2",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"playwright-extra": "^4.3.6",
|
"playwright-extra": "^4.3.6",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
"puppeteer": "^24.30.0",
|
"puppeteer": "^24.30.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
|
|||||||
+157
-6
@@ -1,4 +1,9 @@
|
|||||||
const Tesseract = require('tesseract.js');
|
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 = {
|
const CAPTCHA_PATTERNS = {
|
||||||
recaptcha: {
|
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) {
|
async function detectCaptcha(page) {
|
||||||
const captchaInfo = await page.evaluate((patterns) => {
|
const captchaInfo = await page.evaluate((patterns) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
@@ -103,30 +120,163 @@ async function solveSimpleCaptcha(page) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (imageCaptchaInfo) {
|
if (imageCaptchaInfo) {
|
||||||
console.log(' Attempting OCR-based CAPTCHA solving...');
|
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);
|
||||||
|
|
||||||
|
if (!imageLoaded) {
|
||||||
|
await new Promise(r => setTimeout(r, checkIntervalMs));
|
||||||
|
waitedMs += checkIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const { data: { text } } = await Tesseract.recognize(
|
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,
|
imageCaptchaInfo.src,
|
||||||
'eng',
|
'eng',
|
||||||
{ logger: m => {} }
|
{ logger: m => {} }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(` OCR raw output: "${text}"`);
|
||||||
|
console.log(` OCR confidence: ${confidence}%`);
|
||||||
|
|
||||||
const cleanedText = text.replace(/[^a-zA-Z0-9]/g, '').trim();
|
const cleanedText = text.replace(/[^a-zA-Z0-9]/g, '').trim();
|
||||||
|
|
||||||
if (cleanedText && cleanedText.length >= 3) {
|
if (cleanedText && cleanedText.length >= 3) {
|
||||||
console.log(` OCR read: "${cleanedText}"`);
|
console.log(` OCR fallback read: "${cleanedText}"`);
|
||||||
return {
|
return {
|
||||||
type: 'image',
|
type: 'image',
|
||||||
answer: cleanedText,
|
answer: cleanedText,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
|
confidence: confidence,
|
||||||
|
service: 'tesseract-fallback',
|
||||||
imageSrc: imageCaptchaInfo.src
|
imageSrc: imageCaptchaInfo.src
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (ocrError) {
|
} catch (ocrError) {
|
||||||
console.log(` OCR failed: ${ocrError.message}`);
|
console.log(` OCR fallback also failed: ${ocrError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'image', message: 'Image CAPTCHA detected - OCR failed, manual solving required' };
|
return {
|
||||||
|
type: 'image',
|
||||||
|
answer: null,
|
||||||
|
message: `Both 2Captcha and OCR failed: ${error.message}`,
|
||||||
|
imageSrc: imageCaptchaInfo.src
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -135,5 +285,6 @@ async function solveSimpleCaptcha(page) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
CAPTCHA_PATTERNS,
|
CAPTCHA_PATTERNS,
|
||||||
detectCaptcha,
|
detectCaptcha,
|
||||||
solveSimpleCaptcha
|
solveSimpleCaptcha,
|
||||||
|
fetchImageAsBuffer
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-7
@@ -10,36 +10,39 @@ function createCLI() {
|
|||||||
.option('-f, --file <path>', 'Load sites from text file (one URL per line)')
|
.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('-k, --keep-open', 'Keep browser tabs open after processing for verification')
|
||||||
.option('-l, --log', 'Save output to timestamped log file')
|
.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')
|
.option('-h, --help', 'Show help message')
|
||||||
.argument('[target]', 'Site name or URL to process (filters from config.toml sites)');
|
.argument('[target]', 'Site name or URL to process (filters from config.toml sites)');
|
||||||
|
|
||||||
program.on('option:help', () => {
|
program.on('option:help', () => {
|
||||||
console.log(`
|
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:
|
Options:
|
||||||
-f, --file=<path> Load sites from text file (one URL per line)
|
-f, --file=<path> Load sites from text file (one URL per line)
|
||||||
-k, --keep-open Keep browser tabs open after processing for verification
|
-k, --keep-open Keep browser tabs open after processing for verification
|
||||||
-l, --log Save output to timestamped log file
|
-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
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
node index.js Process all sites from config.toml
|
node index.js Process all sites from config.toml
|
||||||
node index.js --file=sites.txt Process sites from file
|
node index.js --file=sites.txt Process sites from file
|
||||||
node index.js "Dutch News" Process specific site
|
node index.js "Dutch News" Process specific site
|
||||||
node index.js --file=sites.txt --keep-open Keep tabs open for verification
|
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 --log Save output to log file
|
||||||
|
node index.js --dry-run Test without calling 2Captcha API
|
||||||
|
|
||||||
Text file format (sites.txt):
|
Text file format (sites.txt):
|
||||||
# Lines starting with # are comments
|
# Lines starting with # are comments
|
||||||
https://example.com/newsletter
|
https://example.com/newsletter
|
||||||
https://another-site.com/signup
|
https://another-site.com/signup
|
||||||
|
|
||||||
Configuration:
|
Configuration:
|
||||||
Edit config.toml to set credentials and default sites
|
Edit config.toml to set credentials and default sites
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ function parseCLI(args = process.argv.slice(2)) {
|
|||||||
file: opts.file || null,
|
file: opts.file || null,
|
||||||
keepOpen: opts.keepOpen || false,
|
keepOpen: opts.keepOpen || false,
|
||||||
log: opts.log || false,
|
log: opts.log || false,
|
||||||
|
dryRun: opts.dryRun || false,
|
||||||
help: opts.help || false
|
help: opts.help || false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-5
@@ -3,8 +3,9 @@ const { detectFormFields } = require('./fields');
|
|||||||
const { detectSubmitButton } = require('./submit');
|
const { detectSubmitButton } = require('./submit');
|
||||||
const { detectCaptcha, solveSimpleCaptcha } = require('./captcha');
|
const { detectCaptcha, solveSimpleCaptcha } = require('./captcha');
|
||||||
const { classifyNewsletterForm } = require('./classifier');
|
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 siteFailed = false;
|
||||||
let failureReason = '';
|
let failureReason = '';
|
||||||
let page = null;
|
let page = null;
|
||||||
@@ -42,6 +43,9 @@ async function processSite(browser, site, globalCreds, browserConfig, keepOpen =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (captchaInfo.some(c => c.type === 'mathCaptcha')) {
|
if (captchaInfo.some(c => c.type === 'mathCaptcha')) {
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(` [DRY-RUN] Would solve math CAPTCHA`);
|
||||||
|
} else {
|
||||||
const solution = await solveSimpleCaptcha(page);
|
const solution = await solveSimpleCaptcha(page);
|
||||||
if (solution && solution.answer) {
|
if (solution && solution.answer) {
|
||||||
console.log(` Auto-solving math: ${solution.text} = ${solution.answer}`);
|
console.log(` Auto-solving math: ${solution.text} = ${solution.answer}`);
|
||||||
@@ -58,11 +62,32 @@ async function processSite(browser, site, globalCreds, browserConfig, keepOpen =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (captchaInfo.some(c => c.type === 'imageCaptcha')) {
|
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);
|
const solution = await solveSimpleCaptcha(page);
|
||||||
if (solution && solution.type === 'image' && solution.answer) {
|
if (solution && solution.answer) {
|
||||||
console.log(` OCR solved image CAPTCHA: "${solution.answer}"`);
|
console.log(` OCR solved CAPTCHA: "${solution.answer}"`);
|
||||||
try {
|
try {
|
||||||
await page.evaluate((answer) => {
|
await page.evaluate((answer) => {
|
||||||
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], input[id*="captcha"], #captcha, .captcha-input');
|
const input = document.querySelector('input[name*="captcha"], input[class*="captcha"], input[id*="captcha"], #captcha, .captcha-input');
|
||||||
@@ -70,10 +95,14 @@ async function processSite(browser, site, globalCreds, browserConfig, keepOpen =
|
|||||||
input.value = answer;
|
input.value = answer;
|
||||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
console.log(` Filled CAPTCHA with: ${answer}`);
|
||||||
}
|
}
|
||||||
}, solution.answer);
|
}, solution.answer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(` Failed to fill OCR answer: ${e.message}`);
|
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) {
|
if (submitSelector) {
|
||||||
try {
|
try {
|
||||||
|
const urlBeforeSubmit = page.url();
|
||||||
await page.click(submitSelector);
|
await page.click(submitSelector);
|
||||||
console.log(` Submitted (${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) {
|
} catch (submitError) {
|
||||||
siteFailed = true;
|
siteFailed = true;
|
||||||
failureReason = failureReason || `Submit failed: ${submitError.message}`;
|
failureReason = failureReason || `Submit failed: ${submitError.message}`;
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
https://jubina.com/list
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 328 B |
Reference in New Issue
Block a user