initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -0,0 +1,88 @@
|
||||
# Multi-Site Sign-Up Automation Script
|
||||
|
||||
This script automates the process of signing up for multiple websites using a single configuration file. It is designed to be flexible and resilient, using advanced techniques to mimic human behavior.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
The script reads its configuration from the `config.toml` file. It launches a browser using your real user profile to avoid bot detection, and then iterates through the list of sites you've defined, filling out and submitting the sign-up forms.
|
||||
|
||||
---
|
||||
|
||||
## Configuration (`config.toml`)
|
||||
|
||||
This is the main file you will edit to control the script. It has the following sections:
|
||||
|
||||
### 1. `[browser]` Section
|
||||
|
||||
This section controls the browser's behavior.
|
||||
|
||||
- `userDataDir` (Required): The absolute path to your Chrome/Chromium user profile. This is critical for avoiding bot detection.
|
||||
- `userAgent` (Optional): A string to make the browser appear as a specific version (e.g., a Windows machine). If left blank, a random user agent is used.
|
||||
|
||||
**Example:**
|
||||
```toml
|
||||
[browser]
|
||||
userDataDir = "/home/someone/.config/chromium"
|
||||
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
```
|
||||
|
||||
### 2. `[global_credentials]` Section
|
||||
|
||||
Define a default set of credentials here. The script will use these for any site that does not have its own specific credentials defined.
|
||||
|
||||
- `username`
|
||||
- `email`
|
||||
- `password`
|
||||
- `age` (or any other custom field)
|
||||
|
||||
**Example:**
|
||||
```toml
|
||||
[global_credentials]
|
||||
username = "MyGlobalUsername"
|
||||
email = "my.global.email@gmail.com"
|
||||
password = "MyGlobalPassword123!"
|
||||
age = "28"
|
||||
```
|
||||
|
||||
### 3. `[[sites]]` Blocks
|
||||
|
||||
Each site you want to process is defined in its own `[[sites]]` block.
|
||||
|
||||
- `name`: A unique name for the site (e.g., "CameraBoys").
|
||||
- `category`: An organizational tag (e.g., "Social", "Financial").
|
||||
- `url`: The direct URL to the sign-up page.
|
||||
- `needsPreSubmitAction`: Set to `true` for sites that have a GDPR/cookie popup. Currently, this is only configured for "CameraBoys".
|
||||
|
||||
#### `[sites.locators]` Subsection
|
||||
|
||||
This is where you define the form fields for the site. The script will dynamically try to fill **any field you list here**.
|
||||
|
||||
- **Easy Mode:** Simply find the element in your browser's developer tools, right-click, "Copy element", and paste the entire HTML tag as the value.
|
||||
- **Advanced Mode:** You can also use a standard CSS selector.
|
||||
|
||||
**Supported Fields:** `username`, `password`, `email`, `confirmEmail`, `age`, or any other custom field.
|
||||
*Note: For `confirmEmail`, the script will automatically use the `email` value.*
|
||||
|
||||
**Example:**
|
||||
```toml
|
||||
[sites.locators]
|
||||
email = '<input id="23510" class="required email" name="23510" type="text">'
|
||||
confirmEmail = '<input id="confirmEmail" class="email required" name="confirmEmail" type="text">'
|
||||
age = '#user_age' # You can still use CSS selectors
|
||||
submit = '<button type="submit">Subscribe</button>'
|
||||
```
|
||||
|
||||
#### `[sites.credentials]` Subsection (Optional)
|
||||
|
||||
If a site requires a unique login, you can define it here. This will **override** the `[global_credentials]` for this site only.
|
||||
|
||||
**Example:**
|
||||
```toml
|
||||
[sites.credentials]
|
||||
username = "SiteSpecificUser"
|
||||
email = "site.specific.email@gmail.com"
|
||||
password = "SitePassword789"
|
||||
age = "42"
|
||||
```
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Configuration file for the multi-site sign-up script.
|
||||
|
||||
[browser]
|
||||
userDataDir = "/home/someone/.config/chromium"
|
||||
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
||||
|
||||
# Global credentials to be used for any site that doesn't have its own specific credentials.
|
||||
[global_credentials]
|
||||
username = "Thisisgebruikersnaam"
|
||||
email = "thisisanothertestbusiness@hotmail.com"
|
||||
password = "Fuckingstrongpassword123!"
|
||||
phone = "+31610488717"
|
||||
firstName = "Thisfirstname"
|
||||
lastName = "Thislastname"
|
||||
|
||||
[[sites]]
|
||||
name = "Dutch News Test"
|
||||
category = "Newsletter"
|
||||
url = "https://www.dutchnews.nl/dutchnews-newsletter-signup/"
|
||||
needsPreSubmitAction = false
|
||||
# autoDetect is enabled by default
|
||||
|
||||
[[sites]]
|
||||
name = "Immaculate Vegan"
|
||||
category = "Newsletter"
|
||||
url = "https://immaculatevegan.com/blogs/magazine"
|
||||
needsPreSubmitAction = false
|
||||
|
||||
[[sites]]
|
||||
name = "Gate6"
|
||||
category = "Newsletter"
|
||||
url = "https://gate6.vn/"
|
||||
needsPreSubmitAction = false
|
||||
|
||||
[[sites]]
|
||||
name = "Kulala Land"
|
||||
category = "Newsletter"
|
||||
url = "https://kulalaland.com/"
|
||||
needsPreSubmitAction = false
|
||||
|
||||
[[sites]]
|
||||
name = "sneakywholefoods"
|
||||
category = "Newsletter"
|
||||
url = "https://www.sneakywholefoods.com/"
|
||||
needsPreSubmitAction = false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
DATE | TIME | WEBSITE | REASON
|
||||
----------------------------------------------------------------------------------------------------
|
||||
15/02/2026 | 12:40:49 | Dutch News Test (https://www.dut | Form still visible after submission (likely failed)
|
||||
15/02/2026 | 12:40:49 | Immaculate Vegan (https://immacu | Form still visible after submission (likely failed)
|
||||
15/02/2026 | 12:40:49 | Gate6 (https://gate6.vn/) | Form still visible after submission (likely failed)
|
||||
15/02/2026 | 12:40:49 | Kulala Land (https://kulalaland. | No email field detected
|
||||
15/02/2026 | 12:40:49 | sneakywholefoods (https://www.sn | Not a newsletter signup form
|
||||
@@ -0,0 +1,146 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const puppeteer = require('puppeteer-extra');
|
||||
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
const { loadConfig, loadSitesFromFile } = require('./src/config');
|
||||
const { processSite } = require('./src/processor');
|
||||
const { createCLI, parseCLI } = require('./src/cli');
|
||||
|
||||
async function main() {
|
||||
const cliOptions = parseCLI();
|
||||
|
||||
if (cliOptions.help) {
|
||||
createCLI().help();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
let sitesToProcess = config.sites || [];
|
||||
|
||||
if (cliOptions.file) {
|
||||
console.log(`Loading sites from: ${cliOptions.file}`);
|
||||
sitesToProcess = loadSitesFromFile(cliOptions.file);
|
||||
console.log(`Loaded ${sitesToProcess.length} sites from file`);
|
||||
}
|
||||
|
||||
const logFile = cliOptions.log ? path.join(__dirname, `run_${new Date().toISOString().replace(/[:.]/g, '-')}.log`) : null;
|
||||
|
||||
if (cliOptions.target) {
|
||||
sitesToProcess = sitesToProcess.filter(site =>
|
||||
site.name.toLowerCase().includes(cliOptions.target.toLowerCase()) ||
|
||||
site.url.toLowerCase().includes(cliOptions.target.toLowerCase())
|
||||
);
|
||||
if (sitesToProcess.length === 0) {
|
||||
console.error(`No site found matching "${cliOptions.target}"`);
|
||||
return;
|
||||
}
|
||||
console.log(`Targeting ${sitesToProcess.length} site(s)`);
|
||||
}
|
||||
|
||||
if (cliOptions.keepOpen) {
|
||||
console.log('Keep-open mode: Tabs will stay open for verification');
|
||||
}
|
||||
|
||||
const browserConfig = config.browser || {};
|
||||
const globalCreds = config.global_credentials || {};
|
||||
const userDataDir = browserConfig.userDataDir;
|
||||
|
||||
if (!userDataDir) {
|
||||
console.error('userDataDir not set in config.toml');
|
||||
return;
|
||||
}
|
||||
|
||||
if (cliOptions.log && logFile) {
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
const writeToLog = (type, args) => {
|
||||
const message = args.map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg)).join(' ');
|
||||
fs.appendFileSync(logFile, `[${new Date().toISOString()}] [${type}] ${message}\n`);
|
||||
};
|
||||
|
||||
console.log = (...args) => { writeToLog('LOG', args); originalLog.apply(console, args); };
|
||||
console.error = (...args) => { writeToLog('ERROR', args); originalError.apply(console, args); };
|
||||
console.warn = (...args) => { writeToLog('WARN', args); originalWarn.apply(console, args); };
|
||||
console.log(`Logging to: ${logFile}`);
|
||||
}
|
||||
|
||||
const failedSites = [];
|
||||
const processedCount = { success: 0, failed: 0 };
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
userDataDir: userDataDir,
|
||||
args: ['--start-maximized', '--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`\nProcessing ${sitesToProcess.length} sites...\n`);
|
||||
|
||||
const allResults = [];
|
||||
|
||||
for (let i = 0; i < sitesToProcess.length; i++) {
|
||||
const site = sitesToProcess[i];
|
||||
console.log(`[${i + 1}/${sitesToProcess.length}]`);
|
||||
|
||||
const result = await processSite(browser, site, globalCreds, browserConfig, cliOptions.keepOpen, cliOptions.dryRun);
|
||||
|
||||
const siteResult = {
|
||||
name: site.name,
|
||||
url: site.url,
|
||||
success: !result.siteFailed,
|
||||
reason: result.failureReason || 'Success',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
allResults.push(siteResult);
|
||||
|
||||
if (result.siteFailed) {
|
||||
failedSites.push(siteResult);
|
||||
processedCount.failed++;
|
||||
} else {
|
||||
processedCount.success++;
|
||||
}
|
||||
|
||||
// Write results after each site
|
||||
const reportPath = path.join(__dirname, 'site_results.txt');
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-GB');
|
||||
const timeStr = now.toLocaleTimeString('en-GB');
|
||||
|
||||
let reportContent = '';
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
reportContent = 'DATE | TIME | STATUS | WEBSITE | RESULT\n';
|
||||
reportContent += '-'.repeat(110) + '\n';
|
||||
}
|
||||
|
||||
const website = `${site.name} (${site.url})`.substring(0, 32).padEnd(32);
|
||||
const status = siteResult.success ? 'SUCCESS' : 'FAILED';
|
||||
reportContent += `${dateStr} | ${timeStr} | ${status} | ${website} | ${siteResult.reason}\n`;
|
||||
|
||||
fs.appendFileSync(reportPath, reportContent);
|
||||
console.log(` -> Result written to site_results.txt`);
|
||||
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
console.log(`\n${'='.repeat(70)}`);
|
||||
console.log(`Done: ${processedCount.success} success, ${processedCount.failed} failed`);
|
||||
console.log(`Full results: site_results.txt`);
|
||||
|
||||
} catch(error) {
|
||||
console.error('Fatal error:', error.message);
|
||||
} finally {
|
||||
if (cliOptions.keepOpen) {
|
||||
console.log(`\nBrowser kept open for verification`);
|
||||
console.log(` Close browser manually when done`);
|
||||
} else {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,354 @@
|
||||
https://immaculatevegan.com/blogs/magazine
|
||||
https://gate6.vn/
|
||||
https://www.sprintzeal.com/
|
||||
https://kulalaland.com/
|
||||
https://www.goplaces.co.uk/
|
||||
https://www.fiftyfiftybottles.com/
|
||||
https://jubina.com/blog
|
||||
https://www.sneakywholefoods.com/
|
||||
https://vegnews.com/signup
|
||||
https://yonayonabeerworks.com/
|
||||
https://www.louispowersportsonline.com/
|
||||
https://www.hairworthy.co.uk/
|
||||
https://endotaspa.com.au/
|
||||
https://prestigify.com/
|
||||
https://nobones.co/
|
||||
https://www.sofamania.com/
|
||||
https://dimoradellebalze.com/en/restaurant/
|
||||
https://www.42photo.com/
|
||||
https://ifinishedmybasement.com/
|
||||
https://tettixgames.com/
|
||||
https://www.corinnekai.com/raw-honey-raw-self-newsletter/
|
||||
https://www.blendnwhip.com/
|
||||
https://www.brookebowlin.com/nuance-required
|
||||
https://www.thatvegandad.net/
|
||||
https://comhwa.org.au/blog/newsletters/
|
||||
https://hiutdenim.co.uk/
|
||||
https://local-lovely.com/
|
||||
https://vituswholefoods.com/
|
||||
https://theoriginalshabby.it/
|
||||
https://rothys.com/en-au
|
||||
https://www.trustbasket.com/
|
||||
https://chelseabrice.com/
|
||||
https://wolfsgamingblog.com/
|
||||
https://www.leadfuze.com/blog/
|
||||
https://thedefiant.io/
|
||||
https://www.entrepreneur.com/
|
||||
https://pantproject.com/
|
||||
https://www.terrebleu.ca/
|
||||
https://www.shopify.com/in/blog/shopify-stores
|
||||
https://www.skooob.com/
|
||||
https://mremountain.com/
|
||||
https://uncommonjames.com/
|
||||
https://www.axios.com/signup/tampa-bay
|
||||
https://www.dietassassinista.com/blog/tag/African+Cuisine
|
||||
https://miriamporter.ca/
|
||||
https://uppercasemagazine.com/
|
||||
https://www.mercadodasrosas.com/
|
||||
https://mybageecha.com/
|
||||
https://bebemoss.com/
|
||||
https://linkwithin.com/
|
||||
https://www.mediocrechef.com/newsletter
|
||||
https://www.trainheroic.com/blog/rdl-for-posterior-chain-development/
|
||||
https://www.duatravel.com/
|
||||
https://youthiapa.com/
|
||||
https://www.prolinerangehoods.com/
|
||||
https://inthecove.com.au/sign-up-for-newsletter/
|
||||
https://thevegan8.com/
|
||||
https://newsinfrench.org/
|
||||
https://www.wheelpros.com/
|
||||
https://eatbetterco.com/
|
||||
https://www.925express.com/
|
||||
https://www.kurandawholefoods.com.au/blogs/blog-posts
|
||||
https://unitedbyblue.com/
|
||||
https://www.lifx.com.au/
|
||||
https://www.weareunderground.com/
|
||||
https://easyeyesound.com/
|
||||
https://fashionbrainacademy.com/blog-vs-newsletter/
|
||||
https://www.myburntorange.com/
|
||||
https://vituswholefoods.com/blogs/news
|
||||
https://www.givemetap.com/
|
||||
https://www.lunchskins.com/
|
||||
https://www.gymlaunch.com/blog-categories/newsletter
|
||||
https://yumetwins.com/blog/kawaii-hotel-rooms-in-japan
|
||||
https://www.artistsandillustrators.co.uk/
|
||||
https://tokyovegangyoza.com/en/pages/newsletter-registration
|
||||
https://thegoodlifefrance.com/newsletter-sign-up/
|
||||
https://www.novethic.fr/economie-et-social/business-model-en-transition/rse-managers-pilotage-entreprise
|
||||
https://www.35mmc.com/
|
||||
https://therainbowvision.com/
|
||||
https://www.customgoldgrillz.com/
|
||||
https://tofinosoapcompany.com/
|
||||
https://store.lonestarlearning.com/
|
||||
https://mumcentral.com.au/
|
||||
https://www.velasca.com/
|
||||
https://satyaorganics.com/
|
||||
https://popitout.in/
|
||||
https://www.afar.com/travel-tips/the-best-restaurants-in-new-zealand
|
||||
https://www.mindbodygreen.com/
|
||||
https://fourcolumnsofabalancedlife.com/
|
||||
https://www.relishthebite.com/
|
||||
https://www.airportappliance.com/
|
||||
https://quedelibros.com/
|
||||
https://www.gazoakleychef.com/
|
||||
https://egunsifoods.com/
|
||||
https://www.saintalfred.com/
|
||||
https://www.wholisticallyhannah.com/all/welcome-to-wholistically-hannah
|
||||
https://glamout.com.mx/
|
||||
https://mommuseum.org/sign-up-for-the-newsletter/
|
||||
https://www.greenseashells.com/
|
||||
https://jessicainthekitchen.com/
|
||||
https://www.carlazampatti.com.au/
|
||||
https://www.ricarda.com/
|
||||
https://www.supercutekawaii.com/2023/07/kawaii-japan-trip-planning-resources/
|
||||
https://www.devilinspired.com/blog/Harajuku-Fashion-Japanese-Street-Fashion/what-is-jirai-kei.html
|
||||
https://leehamnews.com/
|
||||
https://highfivevape.com/
|
||||
https://www.loveandlemons.com/subscribe/
|
||||
https://finduslost.com/
|
||||
https://makeupformelaningirls.com/
|
||||
https://gosvish.com/
|
||||
https://bruvi.com/
|
||||
https://120percento.com/pages/linen-pants
|
||||
https://www.tsw.com/
|
||||
https://economics-online.myshopify.com/
|
||||
https://www.captaincookcruisesfiji.com/blog/holidaying-in-fiji-8-travel-blogs-to-read
|
||||
https://www.thetravelpockets.com/new-blog/tag/Sanrio
|
||||
https://www.beafunmum.com/
|
||||
https://willowltd.com/
|
||||
https://www.adolfodominguez.com.au/
|
||||
https://laviebami.com/tag/west-african-food/
|
||||
https://childandfamilyblog.com/
|
||||
https://www.thehimalayanorganics.in/
|
||||
https://labelsfashion.com/
|
||||
https://theoverwhelmedmommy.com/
|
||||
https://headsupfortails.com/
|
||||
https://www.healthmates.com.au/about/monthly-newsletter
|
||||
https://www.cgriver.com/
|
||||
https://theholisticgardenblog.com/
|
||||
https://www.theguthealthdoctor.com/subscribe
|
||||
https://djurrattsalliansen.se/hjalp-djuren/lev-djurvanligt/bli-vegan/
|
||||
https://www.blackbykenm.com/
|
||||
https://shop.smeg.com.au/
|
||||
https://hbx.com/?no-direct=1
|
||||
https://bonnelife.com/
|
||||
https://matthiaskamann.com/blog/
|
||||
https://www.justherbs.in/
|
||||
https://gaijinpot.com/
|
||||
https://wolfsgamingblog.com/
|
||||
https://scanwp.net/blog/newsletter-thank-you/
|
||||
https://120percento.com/
|
||||
https://fausto.in/
|
||||
https://sartoriadilusso.shop/designers/off-white
|
||||
https://kawaiibabe.com/
|
||||
https://www.dubwheels.com/
|
||||
https://www.nextfpv.com.au/
|
||||
https://unarosevegan.ca/welcome-to-my-blog-about-una-rose-vegan/
|
||||
https://thecanadianafrican.com/
|
||||
https://kindahome.co.ao/
|
||||
https://clutchchairz.com/
|
||||
https://whereiam.blog/
|
||||
https://www.dandelionchocolate.com/
|
||||
https://zeroinfy.in/
|
||||
https://meowmeowtweet.com/
|
||||
https://www.phoebespurefood.com/
|
||||
https://www.greenheartgames.com/
|
||||
https://freshersjobz.com/
|
||||
https://www.prashantisarees.com/
|
||||
https://www.snitch.co.in/
|
||||
https://www.vervecoffee.com/
|
||||
https://homesteadmuseum.blog/
|
||||
https://bestfriendsclub.ca/
|
||||
https://thenelgroup.au/blogs/news
|
||||
https://www.goodrx.com/health
|
||||
https://barekind.co.uk/
|
||||
https://flourist.com/
|
||||
https://www.belindainternational.com/
|
||||
https://www.wigsandgrace.com/
|
||||
https://www.eatfitgo.com/
|
||||
https://highfivepins.com/
|
||||
https://www.duroflexworld.com/
|
||||
https://blkandbold.com/
|
||||
https://hiyou.co/
|
||||
https://www.respectatwork.gov.au/
|
||||
https://www.thegayvegans.com/general/
|
||||
https://woodfordstatebank.com/investments.html
|
||||
https://elpasomatters.org/2023/01/18/join-el-paso-matters-book-club-celebrate-el-pasos-authors/
|
||||
https://norte.ventures/
|
||||
https://www.rachelcampbellphotography.net/
|
||||
https://www.leppfarmmarket.com/contact/
|
||||
https://panda-patisserie.com/
|
||||
http://studiomspace.ca/
|
||||
https://busbeestyle.com/travel/
|
||||
https://gaysintown.com/
|
||||
https://www.hockerty.com/en-au/blog/tuxedo-vs-suit
|
||||
https://www.everyonehatesmarketers.com/
|
||||
https://chelseabrice.com/
|
||||
https://www.breitbart.com/newsletters/
|
||||
https://www.ableton.com/en/subscribe-to-newsletter/
|
||||
https://www.texastribune.org/
|
||||
https://www.biblechristiansociety.com/newsletter
|
||||
https://biggayhudsonvalley.com/contact/newsletter-signup/
|
||||
https://odin-engineering.dk/nyhedsbrev/
|
||||
https://gardehvalsoe.dk/
|
||||
https://aerligebent.dk/bil-jokes
|
||||
https://eyda.dk/
|
||||
http://yourbusinessgazette.com/
|
||||
https://usacorrespondent.com/
|
||||
https://themarketholders.com/
|
||||
https://tldr.tech/ai
|
||||
https://productmanager.jobs/newsletter
|
||||
https://www.sartoricheese.com/sweepstakes/
|
||||
https://mosbrew.ru/posts/subscribe/
|
||||
https://vua-lya.ru/shares/1289/
|
||||
https://gifts.ru/about/subscribe
|
||||
http://www.auto-neva.ru/rassilki
|
||||
https://www.mann-ivanov-ferber.ru/samorazvitie-books/subscribe/
|
||||
https://sisters-help.ru/
|
||||
https://teatrkukoludm.ru/
|
||||
https://rikkitikkishop.dk/tilmeld-nyhedsbrev/
|
||||
https://dollar-king.net/bear-light-up.html
|
||||
https://fleshbot.com/
|
||||
https://signup.dropout.tv/
|
||||
https://www.salon.com/topic/incels
|
||||
https://pornstar.love/newsletter/
|
||||
https://intothewildernessblog.com/subscribe/
|
||||
https://itslisawelsh.systeme.io/sexual-confidence-guide
|
||||
https://www.familyhandyman.com/newsletter/
|
||||
https://adultindustry.news/
|
||||
https://malestockroom.com/
|
||||
https://www.cindyscharkey.com/newsletter
|
||||
https://www.matthewlyonsauthor.com/newsletter
|
||||
https://pamutlabor.hu/shop/hobbi-erdeklodes-noi-hosszitott-polo
|
||||
https://assodeclic.com/
|
||||
https://www.xexo.it/calendari/4494-calendario-porn-2019.html
|
||||
https://www.siciliafoodporn.it/27-selezione-sicilia-food-porn
|
||||
https://www.secrets-center.ru/
|
||||
https://www.vkostume.ru/catalog/podarki/
|
||||
https://ml.city.yokohama.lg.jp/sympa/subscribe/morinews?previous_action=info
|
||||
https://ucon-acrobatics.jp/pages/newsletter
|
||||
https://www.hover.com/newsletter
|
||||
https://okcthunderwire.usatoday.com/newsletters
|
||||
https://www.philips.co.jp/myphilips/newsletter-registration-campaign17
|
||||
https://www.marni.com/ja-jp/
|
||||
https://www.hasselblad.com/ja-jp/about/contact/
|
||||
https://kuwaii.com.au/
|
||||
https://ideasforgood.jp/mailmagazine/
|
||||
https://www.buscounchollo.com/
|
||||
https://www.uniqlo.com/es/es/content/bienvenido.html
|
||||
https://www.hiperdino.es/folletos/
|
||||
https://darlingillustrations.com/newsletter-subscription/
|
||||
https://gnaraloo.org/subscribe-to-the-gnaraloo-wilderness-foundation-newsletter/
|
||||
https://www.farmandwilderness.org/newsletter-signup
|
||||
https://www.waxit.com.au/pages/waxit-newsletter
|
||||
https://practicalmotoring.com.au/newsletter-subscription/
|
||||
https://www.niba.com.au/newsletter
|
||||
https://www.iaisweb.org/news-and-events/newsletter/newsletter-subscribe/
|
||||
https://crooked.com/tag/incels/
|
||||
https://www.maltego.com/blog/everything-to-know-about-ransomware/
|
||||
https://thewalrus.ca/tag/immigration/
|
||||
https://www.yourdarlingstyle.com/newsletter
|
||||
https://aidendarlingharbour.com.au/sign-up/
|
||||
https://darlingshine.com/pages/watch-us
|
||||
https://www.monaropost.com.au/newsletter-sign-up-form
|
||||
https://www.csiro.au/
|
||||
https://www.darlingwest.com.au/
|
||||
https://www.ivywood.ie/shop/events/a-making-herbal-remedies-practical-course-detail
|
||||
https://www.ninasjewellery.com.au/
|
||||
https://thekawaiishoppu.com/en-au/blogs/news
|
||||
https://www.pernillecorydon.com/subscribe-to-newsletter
|
||||
https://www.tokudeals.com/
|
||||
https://hands.com.au/
|
||||
https://medicalxpress.com/tags/herbal+medicines/
|
||||
https://www.coffeeco.com.au/
|
||||
https://urbanattitude.com.au/
|
||||
https://albathermalsprings.com.au/alba-experiences/spa/herbal-healer-90-min/
|
||||
https://www.adultshop.com/
|
||||
https://soapsweetie.com/
|
||||
https://metropolisbookshop.com.au/
|
||||
https://momnemcoffee.com/pages/newsletter
|
||||
https://www.ricky.com/pages/newsletter
|
||||
https://www.vittoriacoffee.com/blogs/news/tagged/coffee-blog
|
||||
https://www.comeasyouare.com/pages/subscribe-to-our-newsletter
|
||||
https://lioness.io/pages/subscribe
|
||||
https://www.sexshop365.co.uk/catalog/newsletters.php
|
||||
https://www.marelljewellery.com.au/newsletter
|
||||
https://www.bevilles.com.au/
|
||||
https://www.bvibe.com/subscribe
|
||||
https://www.kawaiibox.com/
|
||||
https://www.jekkas.com/
|
||||
https://thedistrictherbalist.co.uk/herbal-resources/newsletter/
|
||||
https://www.seattlecoffeegear.com/pages/subscribe
|
||||
https://sokawaii.com/
|
||||
https://yumetwins.com/
|
||||
https://sararobertsson.com/pages/newsletter
|
||||
https://www.steepedcontent.com/
|
||||
https://www.passionfruitshop.com.au/
|
||||
https://www.kiwiherb.com.au/contact/subscribe-to/
|
||||
https://www.nataliemariejewellery.com/
|
||||
https://alternativebrewing.com.au/blogs/news
|
||||
https://www.sorujewellery.com/en-au/pages/sign-up
|
||||
https://coffeealchemy.com.au/blogs/news
|
||||
https://seaglassjewellery.com.au/newsletter-sign-up/
|
||||
https://www.atlantacoffeeshops.com/newsletter
|
||||
https://www.bibivandervelden.com/pages/newsletter
|
||||
https://joycejewellery.com/pages/signup-newsletter-free-postage
|
||||
https://theherborium.com.au/collections/herbal-remedies
|
||||
https://www.grahams.com.au/
|
||||
https://www.the-herb-guide.com/herb-guide-newsletter.html
|
||||
https://embracejewellery.com/pages/subscribe
|
||||
https://www.aldeacoffee.com/blogs/news
|
||||
https://crystalheavenjewellery.com/en-au/pages/newsletter
|
||||
https://www.jewellerssupplies.com.au/
|
||||
https://www.herbcottage.com.au/pages/herb-lovers-community-newsletter
|
||||
https://shop.thegrounds.com.au/
|
||||
https://www.vakantiestunt.nl/nieuwsbrief.php
|
||||
https://www.vakantieveilingen.nl/nieuwsbrief.html
|
||||
https://nl.secretescapes.com/aanbiedingen
|
||||
https://www.lepetitmarche.com.au/newsletter
|
||||
https://www.lalunapetcare.com.au/subscribe
|
||||
https://larochere-na.com/pages/newsletter
|
||||
https://store.latimes.com/
|
||||
https://www.amiparis.com/au/
|
||||
https://filterstore.com.au/pages/newsletter
|
||||
https://www.versusmerch.com/pages/subscribe-to-our-newsletter
|
||||
https://www.passionandpresence.com/newsletter-signup/
|
||||
https://www.lelo.com/blog/newsletter/
|
||||
https://www.tootsiescabaret.com/
|
||||
https://www.pimpbangkok.com/
|
||||
https://matadornetwork.com/
|
||||
https://pragueforadults.com/category/adult/strip-clubs/
|
||||
https://gayforgood.org/join-our-mailing-list/
|
||||
https://www.postcardmania.com/products-services/lgbt-mailing-list/
|
||||
https://gaygaddis.com/newsletter-subscribe/
|
||||
https://www.gayparentstobe.com/join-community
|
||||
https://www.dunasmap.com/newsletter
|
||||
http://www.pridecenterwny.org/Newsletter
|
||||
https://www.lgbtgreat.com/newsletter
|
||||
https://www.italygaytravels.com/newsletter/
|
||||
https://www.glbthistory.org/newsletter
|
||||
https://www.chirale.it/iscriviti-alla-nostra-newsletter/
|
||||
https://www.ail.it/newsletter
|
||||
https://www.fondazionebrodolini.it/iscriviti-alla-nostra-newsletter
|
||||
https://sex-matters.org/take-action/sign-up/
|
||||
https://link.menshealth.com/join/5u0/thehookupsignup
|
||||
https://whiskeydanduncensored.com/newsletter-socialmedia
|
||||
https://rebelliousmagazine.com/sex-rebelle/
|
||||
https://allbodies.com/newsletter-sign-up/
|
||||
https://sexwithemily.com/subscribe/
|
||||
https://www.wonderlandbakery.com/
|
||||
https://www.carlybloombooks.com/the-bloomin-news/
|
||||
https://www.slaa.org.au/subscribe/
|
||||
https://www.onlyfriends.com.au/
|
||||
https://www.clubx.com.au/
|
||||
https://booko.com.au/
|
||||
https://sextoyserotica.com.au/
|
||||
https://hawttt.com.au/
|
||||
https://www.filmink.com.au/best-hookup-sites-australia-13-free-aussie-dating-apps/
|
||||
https://www.ricksnewyork.com/
|
||||
https://www.clubonyxcharlotte.com/
|
||||
https://www.goldclubcenterfolds.com/
|
||||
https://www.labohemegc.com/
|
||||
https://www.rickschicago.com/
|
||||
https://thewalrus.ca/welcome-to-the-manor-church-by-day-strip-club-by-night/
|
||||
https://vintagegaybooks.substack.com/
|
||||
Generated
+2114
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "sign_up_email",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@2captcha/captcha-solver": "^1.3.2",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"2captcha": "^3.0.5-2",
|
||||
"axios": "^1.13.5",
|
||||
"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",
|
||||
"tesseract.js": "^7.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
DATE | TIME | STATUS | WEBSITE | RESULT
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
15/02/2026 | 12:55:22 | SUCCESS | Dutch News Test (https://www.dut | Success
|
||||
15/02/2026 | 12:55:27 | SUCCESS | Immaculate Vegan (https://immacu | Success
|
||||
15/02/2026 | 12:55:30 | FAILED | Gate6 (https://gate6.vn/) | Navigating frame was detached
|
||||
15/02/2026 | 12:55:31 | FAILED | Kulala Land (https://kulalaland. | Connection closed.
|
||||
15/02/2026 | 12:56:00 | SUCCESS | Immaculate Vegan (https://immacu | Success
|
||||
15/02/2026 | 13:05:01 | FAILED | Immaculate Vegan (https://immacu | hCaptcha solving failed
|
||||
15/02/2026 | 13:07:45 | FAILED | Immaculate Vegan (https://immacu | hCaptcha solving failed
|
||||
15/02/2026 | 13:10:49 | FAILED | Immaculate Vegan (https://immacu | hCaptcha solving failed
|
||||
15/02/2026 | 13:18:48 | FAILED | Immaculate Vegan (https://immacu | hCaptcha solving failed
|
||||
15/02/2026 | 13:25:45 | FAILED | Site_1 (https://immaculatevegan. | hCaptcha solving failed
|
||||
15/02/2026 | 13:25:52 | SUCCESS | Site_2 (https://gate6.vn/) | Success
|
||||
15/02/2026 | 13:26:27 | SUCCESS | Site_3 (https://www.sprintzeal.c | Success
|
||||
15/02/2026 | 13:26:30 | FAILED | Site_4 (https://kulalaland.com/) | No email field detected
|
||||
15/02/2026 | 13:26:35 | SUCCESS | Site_5 (https://www.goplaces.co. | Success
|
||||
15/02/2026 | 13:26:43 | SUCCESS | Site_6 (https://www.fiftyfiftybo | Success
|
||||
15/02/2026 | 13:26:47 | FAILED | Site_7 (https://jubina.com/blog) | No email field detected
|
||||
15/02/2026 | 13:26:53 | FAILED | Site_8 (https://www.sneakywholef | Not a newsletter signup form
|
||||
15/02/2026 | 13:26:58 | SUCCESS | Site_9 (https://vegnews.com/sign | Success
|
||||
15/02/2026 | 13:27:04 | SUCCESS | Site_10 (https://yonayonabeerwor | Success
|
||||
15/02/2026 | 13:27:21 | SUCCESS | Site_11 (https://www.louispowers | Success
|
||||
15/02/2026 | 13:27:28 | SUCCESS | Site_12 (https://www.hairworthy. | Success
|
||||
15/02/2026 | 13:27:34 | FAILED | Site_13 (https://endotaspa.com.a | Not a newsletter signup form
|
||||
15/02/2026 | 13:27:39 | SUCCESS | Site_14 (https://prestigify.com/ | Success
|
||||
15/02/2026 | 13:27:42 | FAILED | Site_15 (https://nobones.co/) | No email field detected
|
||||
15/02/2026 | 13:28:01 | SUCCESS | Site_16 (https://www.sofamania.c | Success
|
||||
15/02/2026 | 13:28:12 | FAILED | Site_17 (https://dimoradellebalz | CAPTCHA present: reCAPTCHA
|
||||
15/02/2026 | 13:28:25 | SUCCESS | Site_18 (https://www.42photo.com | Success
|
||||
15/02/2026 | 13:28:27 | FAILED | Site_19 (https://ifinishedmybase | Execution context was destroyed, most likely because of a navigation.
|
||||
15/02/2026 | 13:28:30 | FAILED | Site_20 (https://tettixgames.com | Navigating frame was detached
|
||||
15/02/2026 | 13:28:31 | FAILED | Site_21 (https://www.corinnekai. | Connection closed.
|
||||
15/02/2026 | 13:29:19 | FAILED | Site_1 (https://immaculatevegan. | hCaptcha solving failed
|
||||
15/02/2026 | 13:29:25 | SUCCESS | Site_2 (https://gate6.vn/) | Success
|
||||
15/02/2026 | 13:29:31 | SUCCESS | Site_3 (https://www.sprintzeal.c | Success
|
||||
15/02/2026 | 13:29:45 | FAILED | Site_4 (https://kulalaland.com/) | No email field detected
|
||||
15/02/2026 | 13:29:50 | SUCCESS | Site_5 (https://www.goplaces.co. | Success
|
||||
15/02/2026 | 13:30:06 | SUCCESS | Site_6 (https://www.fiftyfiftybo | Success
|
||||
15/02/2026 | 13:30:09 | FAILED | Site_7 (https://jubina.com/blog) | No email field detected
|
||||
15/02/2026 | 13:30:27 | FAILED | Site_8 (https://www.sneakywholef | Not a newsletter signup form
|
||||
15/02/2026 | 13:30:38 | SUCCESS | Site_9 (https://vegnews.com/sign | Success
|
||||
15/02/2026 | 13:30:43 | SUCCESS | Site_10 (https://yonayonabeerwor | Success
|
||||
15/02/2026 | 13:30:47 | SUCCESS | Site_11 (https://www.louispowers | Success
|
||||
15/02/2026 | 13:30:57 | SUCCESS | Site_12 (https://www.hairworthy. | Success
|
||||
15/02/2026 | 13:31:03 | FAILED | Site_13 (https://endotaspa.com.a | Not a newsletter signup form
|
||||
15/02/2026 | 13:31:14 | SUCCESS | Site_14 (https://prestigify.com/ | Success
|
||||
15/02/2026 | 13:31:16 | FAILED | Site_15 (https://nobones.co/) | No email field detected
|
||||
15/02/2026 | 13:31:22 | SUCCESS | Site_16 (https://www.sofamania.c | Success
|
||||
15/02/2026 | 13:31:29 | FAILED | Site_17 (https://dimoradellebalz | CAPTCHA present: reCAPTCHA
|
||||
15/02/2026 | 13:31:34 | SUCCESS | Site_18 (https://www.42photo.com | Success
|
||||
15/02/2026 | 13:31:37 | FAILED | Site_19 (https://ifinishedmybase | Execution context was destroyed, most likely because of a navigation.
|
||||
15/02/2026 | 13:31:41 | SUCCESS | Site_20 (https://tettixgames.com | Success
|
||||
15/02/2026 | 13:31:48 | SUCCESS | Site_21 (https://www.corinnekai. | Success
|
||||
15/02/2026 | 13:31:59 | SUCCESS | Site_22 (https://www.blendnwhip. | Success
|
||||
15/02/2026 | 13:32:04 | FAILED | Site_23 (https://www.brookebowli | Submit failed: SyntaxError: Failed to execute 'querySelector' on 'Document': '.
|
||||
' is not a valid selector.
|
||||
15/02/2026 | 13:32:07 | FAILED | Site_24 (https://www.thatveganda | Submit failed: SyntaxError: Failed to execute 'querySelector' on 'Document': '.
|
||||
' is not a valid selector.
|
||||
15/02/2026 | 13:32:14 | SUCCESS | Site_25 (https://comhwa.org.au/b | Success
|
||||
15/02/2026 | 13:32:18 | FAILED | Site_26 (https://hiutdenim.co.uk | Submit failed: Node is either not clickable or not an Element
|
||||
15/02/2026 | 13:32:24 | SUCCESS | Site_27 (https://local-lovely.co | Success
|
||||
15/02/2026 | 13:32:40 | FAILED | Site_28 (https://vituswholefoods | Submit failed: No element found for selector: button:nth-of-type(0)
|
||||
15/02/2026 | 13:32:45 | SUCCESS | Site_29 (https://theoriginalshab | Success
|
||||
15/02/2026 | 13:32:52 | FAILED | Site_30 (https://rothys.com/en-a | No email field detected
|
||||
15/02/2026 | 13:32:59 | FAILED | Site_31 (https://www.trustbasket | Not a newsletter signup form
|
||||
15/02/2026 | 13:33:02 | FAILED | Site_32 (https://chelseabrice.co | Submit failed: SyntaxError: Failed to execute 'querySelector' on 'Document': '.
|
||||
' is not a valid selector.
|
||||
15/02/2026 | 13:33:06 | SUCCESS | Site_33 (https://wolfsgamingblog | Success
|
||||
15/02/2026 | 13:33:11 | FAILED | Site_34 (https://www.leadfuze.co | No email field detected
|
||||
15/02/2026 | 13:33:16 | FAILED | Site_35 (https://thedefiant.io/) | No email field detected
|
||||
15/02/2026 | 13:33:22 | FAILED | Site_36 (https://www.entrepreneu | No email field detected
|
||||
15/02/2026 | 13:33:32 | SUCCESS | Site_37 (https://pantproject.com | Success
|
||||
15/02/2026 | 13:33:38 | FAILED | Site_38 (https://www.terrebleu.c | No email field detected
|
||||
15/02/2026 | 13:33:41 | FAILED | Site_39 (https://www.shopify.com | Failed to fill email: SyntaxError: Failed to execute 'querySelector' on 'Document': '#email-input-:Rdbh4ll5H1:' is not a valid selector.
|
||||
15/02/2026 | 13:33:47 | SUCCESS | Site_40 (https://www.skooob.com/ | Success
|
||||
15/02/2026 | 13:33:52 | SUCCESS | Site_41 (https://mremountain.com | Success
|
||||
15/02/2026 | 13:34:06 | SUCCESS | Site_42 (https://uncommonjames.c | Success
|
||||
15/02/2026 | 13:34:11 | FAILED | Site_43 (https://www.axios.com/s | CAPTCHA present: reCAPTCHA
|
||||
15/02/2026 | 13:34:14 | FAILED | Site_44 (https://www.dietassassi | Submit failed: SyntaxError: Failed to execute 'querySelector' on 'Document': '.
|
||||
' is not a valid selector.
|
||||
15/02/2026 | 13:34:19 | SUCCESS | Site_45 (https://miriamporter.ca | Success
|
||||
15/02/2026 | 13:34:23 | FAILED | Site_46 (https://uppercasemagazi | No email field detected
|
||||
15/02/2026 | 13:34:29 | SUCCESS | Site_47 (https://www.mercadodasr | Success
|
||||
15/02/2026 | 13:34:35 | SUCCESS | Site_48 (https://mybageecha.com/ | Success
|
||||
15/02/2026 | 13:34:48 | FAILED | Site_49 (https://bebemoss.com/) | Submit failed: Node is either not clickable or not an Element
|
||||
15/02/2026 | 13:34:50 | FAILED | Site_50 (https://linkwithin.com/ | net::ERR_CONNECTION_RESET at https://linkwithin.com/
|
||||
15/02/2026 | 13:43:01 | FAILED | Immaculate Vegan (https://immacu | Navigating frame was detached
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
const Tesseract = require('tesseract.js');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
const { Solver } = require('@2captcha/captcha-solver');
|
||||
const solver = new Solver('9ed0ef51badf9a017ac50aea413d8001');
|
||||
|
||||
const CAPTCHA_PATTERNS = {
|
||||
recaptcha: {
|
||||
selectors: ['.g-recaptcha', '[data-sitekey]', 'iframe[src*="recaptcha"]', '#recaptcha'],
|
||||
name: 'reCAPTCHA'
|
||||
},
|
||||
hcaptcha: {
|
||||
selectors: ['.h-captcha', '[data-hcaptcha-sitekey]', 'iframe[src*="hcaptcha"]', '#hcaptcha', '[data-sitekey*="hcaptcha"]', '.hcaptcha-checkbox'],
|
||||
name: 'hCaptcha'
|
||||
},
|
||||
textCaptcha: {
|
||||
selectors: ['input[name*="captcha"]', 'input[id*="captcha"]', '.captcha-input', '#captcha'],
|
||||
name: 'Text CAPTCHA'
|
||||
},
|
||||
imageCaptcha: {
|
||||
selectors: ['img[src*="captcha"]', '.captcha-image', '#captcha-image', 'img[alt*="captcha" i]'],
|
||||
name: 'Image CAPTCHA'
|
||||
},
|
||||
mathCaptcha: {
|
||||
selectors: ['.math-captcha', '[name*="math"]', 'input[placeholder*="math" i]'],
|
||||
name: 'Math CAPTCHA'
|
||||
}
|
||||
};
|
||||
|
||||
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 = [];
|
||||
|
||||
for (const [type, config] of Object.entries(patterns)) {
|
||||
for (const selector of config.selectors) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if (elements.length > 0) {
|
||||
for (const el of elements) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isVisible = style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0' &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0;
|
||||
|
||||
results.push({
|
||||
type: type,
|
||||
name: config.name,
|
||||
selector: selector,
|
||||
count: elements.length,
|
||||
isVisible: isVisible
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}, CAPTCHA_PATTERNS);
|
||||
|
||||
return captchaInfo;
|
||||
}
|
||||
|
||||
async function solveSimpleCaptcha(page) {
|
||||
const mathSolution = await page.evaluate(() => {
|
||||
const mathText = document.querySelector('.math-captcha, .captcha-question, [class*="math"]');
|
||||
if (mathText) {
|
||||
const text = mathText.textContent || '';
|
||||
const match = text.match(/(\d+)\s*([+\-*/])\s*(\d+)/);
|
||||
if (match) {
|
||||
const [_, a, op, b] = match;
|
||||
let result;
|
||||
switch(op) {
|
||||
case '+': result = parseInt(a) + parseInt(b); break;
|
||||
case '-': result = parseInt(a) - parseInt(b); break;
|
||||
case '*': result = parseInt(a) * parseInt(b); break;
|
||||
case '/': result = Math.floor(parseInt(a) / parseInt(b)); break;
|
||||
}
|
||||
return { type: 'math', answer: result, text: text };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (mathSolution) {
|
||||
return mathSolution;
|
||||
}
|
||||
|
||||
const imageCaptchaInfo = await page.evaluate(() => {
|
||||
const selectors = [
|
||||
'img[src*="captcha"]',
|
||||
'.captcha-image img',
|
||||
'#captcha-image',
|
||||
'img[alt*="captcha" i]',
|
||||
'img[class*="captcha" i]'
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const img = document.querySelector(selector);
|
||||
if (img && img.src) {
|
||||
return { src: img.src, selector: selector };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (imageCaptchaInfo) {
|
||||
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 {
|
||||
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({
|
||||
body: imageData,
|
||||
numeric: 0,
|
||||
min_len: 1,
|
||||
max_len: 10,
|
||||
phrase: 0,
|
||||
caseSensitive: 1,
|
||||
calc: 0,
|
||||
lang: 'en'
|
||||
});
|
||||
|
||||
console.log(` 2Captcha solved: "${result}"`);
|
||||
return {
|
||||
type: 'image',
|
||||
answer: result.trim(),
|
||||
originalText: result,
|
||||
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;
|
||||
}
|
||||
|
||||
async function solveHCaptcha(page, pageUrl) {
|
||||
try {
|
||||
// First, try to find and click the hCaptcha checkbox directly via JavaScript
|
||||
console.log(` Attempting to click hCaptcha checkbox directly...`);
|
||||
|
||||
const clickResult = await page.evaluate(() => {
|
||||
// Try multiple approaches to check the checkbox
|
||||
const selectors = [
|
||||
'#checkbox',
|
||||
'.hcaptcha-checkbox',
|
||||
'[class*="hcaptcha"]',
|
||||
'[role="checkbox"]',
|
||||
'div[aria-haspopup="true"]'
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
// Try clicking it
|
||||
el.click();
|
||||
|
||||
// Also try setting aria-checked
|
||||
el.setAttribute('aria-checked', 'true');
|
||||
|
||||
// Check if it has a checked state now
|
||||
const checked = el.getAttribute('aria-checked');
|
||||
return { clicked: true, selector: selector, ariaChecked: checked };
|
||||
}
|
||||
}
|
||||
return { clicked: false };
|
||||
});
|
||||
|
||||
console.log(` Click result:`, clickResult);
|
||||
|
||||
// Wait a moment and check if hCaptcha is now checked
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
const hCaptchaChecked = await page.evaluate(() => {
|
||||
const checkbox = document.querySelector('#checkbox, .hcaptcha-checkbox, [role="checkbox"]');
|
||||
if (checkbox) {
|
||||
return checkbox.getAttribute('aria-checked') === 'true';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hCaptchaChecked) {
|
||||
console.log(` hCaptcha checkbox checked successfully!`);
|
||||
return { type: 'hcaptcha', answer: 'clicked', service: 'direct-click' };
|
||||
}
|
||||
|
||||
// If direct click didn't work, try 2Captcha API
|
||||
console.log(` Direct click didn't work, trying 2Captcha API...`);
|
||||
const hcaptchaInfo = await page.evaluate(() => {
|
||||
// Look for sitekey in data attributes
|
||||
const dataSelectors = [
|
||||
'[data-hcaptcha-sitekey]',
|
||||
'[data-sitekey]',
|
||||
'.h-captcha',
|
||||
'#hcaptcha',
|
||||
'[class*="hcaptcha"]'
|
||||
];
|
||||
|
||||
for (const selector of dataSelectors) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
for (const el of elements) {
|
||||
const sitekey = el.getAttribute('data-hcaptcha-sitekey') || el.getAttribute('data-sitekey');
|
||||
if (sitekey && sitekey.length > 10) {
|
||||
return { sitekey: sitekey, selector: selector };
|
||||
}
|
||||
// Check parent chain
|
||||
let parent = el.parentElement;
|
||||
for (let i = 0; i < 5 && parent; i++) {
|
||||
const parentKey = parent.getAttribute('data-hcaptcha-sitekey') || parent.getAttribute('data-sitekey');
|
||||
if (parentKey && parentKey.length > 10) {
|
||||
return { sitekey: parentKey, selector: selector };
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for hCaptcha iframe and get sitekey from URL
|
||||
const iframe = document.querySelector('iframe[src*="hcaptcha"]');
|
||||
if (iframe) {
|
||||
const src = iframe.getAttribute('src') || '';
|
||||
const match = src.match(/sitekey=([^&]+)/);
|
||||
if (match && match[1]) {
|
||||
return { sitekey: match[1], selector: 'iframe[src*="hcaptcha"]' };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search entire HTML for hCaptcha sitekey pattern
|
||||
const html = document.documentElement.innerHTML;
|
||||
const sitekeyMatch = html.match(/["']sitekey["']\s*[:=]\s*["']([a-zA-Z0-9_-]{20,})["']/);
|
||||
if (sitekeyMatch && sitekeyMatch[1]) {
|
||||
return { sitekey: sitekeyMatch[1], selector: 'html-content' };
|
||||
}
|
||||
|
||||
// Look for hcap sitekey
|
||||
const hcapMatch = html.match(/hcap[tT]cha.*?["']sitekey["']\s*[:=]\s*["']([a-zA-Z0-9_-]{20,})["']/);
|
||||
if (hcapMatch && hcapMatch[1]) {
|
||||
return { sitekey: hcapMatch[1], selector: 'html-script' };
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!hcaptchaInfo) {
|
||||
console.log(` hCaptcha not found on page`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(` Found hCaptcha sitekey: ${hcaptchaInfo.sitekey}`);
|
||||
console.log(` Submitting to 2Captcha...`);
|
||||
|
||||
// Use new @2captcha/captcha-solver package with object syntax
|
||||
const result = await solver.hcaptcha({
|
||||
pageurl: pageUrl,
|
||||
sitekey: hcaptchaInfo.sitekey
|
||||
});
|
||||
|
||||
console.log(` hCaptcha solved: ${result}`);
|
||||
|
||||
// Try multiple methods to set the token
|
||||
await page.evaluate((token) => {
|
||||
// Method 1: Find textarea
|
||||
const textarea = document.querySelector('textarea[name="h-captcha-response"]');
|
||||
if (textarea) {
|
||||
textarea.value = token;
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
console.log(' Set token in textarea');
|
||||
}
|
||||
|
||||
// Method 2: Find iframe and set directly
|
||||
const iframe = document.querySelector('iframe[src*="hcaptcha"]');
|
||||
if (iframe) {
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
const iframeTextarea = iframeDoc.querySelector('textarea[name="h-captcha-response"]');
|
||||
if (iframeTextarea) {
|
||||
iframeTextarea.value = token;
|
||||
iframeTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
console.log(' Set token in iframe textarea');
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(' Cannot access iframe directly');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Set on any hCaptcha related element
|
||||
document.querySelectorAll('[id*="hcaptcha"], [class*="hcaptcha"]').forEach(el => {
|
||||
if (el.value !== undefined) el.value = token;
|
||||
if (el.dataset.response) el.dataset.response = token;
|
||||
});
|
||||
}, result.data);
|
||||
|
||||
return {
|
||||
type: 'hcaptcha',
|
||||
answer: result.data,
|
||||
service: '2captcha'
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMsg = error.message || '';
|
||||
|
||||
if (errorMsg.includes('ERROR_METHOD_CALL') || errorMsg.includes('Unexpected Error')) {
|
||||
console.log(` ERROR: hCaptcha not supported by your 2Captcha API key`);
|
||||
console.log(` Your API key may not include hCaptcha solving.`);
|
||||
console.log(` Options:`);
|
||||
console.log(` - Contact 2Captcha to add hCaptcha to your plan`);
|
||||
console.log(` - Use an alternative service like CapSolver or Anti-Captcha`);
|
||||
} else {
|
||||
console.log(` hCaptcha solving failed: ${errorMsg}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CAPTCHA_PATTERNS,
|
||||
detectCaptcha,
|
||||
solveSimpleCaptcha,
|
||||
solveHCaptcha,
|
||||
fetchImageAsBuffer
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
async function classifyNewsletterForm(page) {
|
||||
const formInfo = await page.evaluate(() => {
|
||||
const results = {
|
||||
hasEmailInput: false,
|
||||
hasNewsletterKeywords: false,
|
||||
hasSearchInput: false,
|
||||
hasLoginInput: false,
|
||||
hasContactForm: false,
|
||||
hasOnlyEmail: false,
|
||||
formCount: 0,
|
||||
emailInputCount: 0,
|
||||
totalInputCount: 0,
|
||||
confidence: 0,
|
||||
reasons: [],
|
||||
signals: {
|
||||
positive: [],
|
||||
negative: []
|
||||
}
|
||||
};
|
||||
|
||||
const allInputs = document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"])');
|
||||
results.totalInputCount = allInputs.length;
|
||||
|
||||
const emailInputs = document.querySelectorAll('input[type="email"], input[name*="email" i], input[id*="email" i], input[placeholder*="email" i]');
|
||||
results.emailInputCount = emailInputs.length;
|
||||
results.hasEmailInput = emailInputs.length > 0;
|
||||
if (results.hasEmailInput) {
|
||||
results.signals.positive.push(`Found ${emailInputs.length} email input(s)`);
|
||||
}
|
||||
|
||||
const pageText = document.body.innerText.toLowerCase();
|
||||
const titleText = document.title.toLowerCase();
|
||||
const combinedText = pageText + ' ' + titleText;
|
||||
|
||||
const newsletterKeywords = [
|
||||
'newsletter', 'subscribe', 'signup', 'sign up', 'join our', 'stay updated',
|
||||
'get updates', 'email updates', 'weekly digest', 'monthly newsletter',
|
||||
'sign up for updates', 'join the list', 'email list', 'mailing list'
|
||||
];
|
||||
|
||||
for (const keyword of newsletterKeywords) {
|
||||
if (combinedText.includes(keyword)) {
|
||||
results.hasNewsletterKeywords = true;
|
||||
results.signals.positive.push(`Keyword: "${keyword}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const searchInputs = document.querySelectorAll('input[type="search"], input[name*="search" i], input[id*="search" i], input[placeholder*="search" i]');
|
||||
results.hasSearchInput = searchInputs.length > 0;
|
||||
if (results.hasSearchInput) {
|
||||
results.signals.negative.push('Search input detected');
|
||||
results.reasons.push('Search input detected');
|
||||
}
|
||||
|
||||
const passwordInputs = document.querySelectorAll('input[type="password"]');
|
||||
const usernameInputs = document.querySelectorAll('input[name*="username" i], input[name*="login" i], input[id*="username" i]');
|
||||
results.hasLoginInput = passwordInputs.length > 0 || usernameInputs.length > 0;
|
||||
if (results.hasLoginInput) {
|
||||
results.signals.negative.push('Login/password fields detected');
|
||||
results.reasons.push('Login/password fields detected');
|
||||
}
|
||||
|
||||
const contactKeywords = ['contact us', 'get in touch', 'send message', 'inquiry'];
|
||||
for (const keyword of contactKeywords) {
|
||||
if (combinedText.includes(keyword)) {
|
||||
results.hasContactForm = true;
|
||||
results.signals.negative.push(`Contact form keyword: "${keyword}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const nonEmailInputs = Array.from(allInputs).filter(input => {
|
||||
const type = input.type?.toLowerCase();
|
||||
const name = input.name?.toLowerCase() || '';
|
||||
return type !== 'email' && !name.includes('email');
|
||||
});
|
||||
|
||||
results.hasOnlyEmail = results.hasEmailInput && nonEmailInputs.length === 0;
|
||||
if (results.hasOnlyEmail) {
|
||||
results.signals.positive.push('Only email field (no other inputs)');
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
if (results.hasEmailInput) score += 40;
|
||||
if (results.hasNewsletterKeywords) score += 35;
|
||||
if (results.hasOnlyEmail) score += 15;
|
||||
if (results.totalInputCount <= 3) score += 10;
|
||||
|
||||
if (!results.hasSearchInput) score += 10;
|
||||
if (!results.hasLoginInput) score += 15;
|
||||
if (!results.hasContactForm) score += 5;
|
||||
|
||||
if (results.hasSearchInput) score -= 30;
|
||||
if (results.hasLoginInput) score -= 50;
|
||||
if (results.hasContactForm && !results.hasNewsletterKeywords) score -= 20;
|
||||
|
||||
results.confidence = Math.max(0, Math.min(100, score));
|
||||
results.isNewsletter = results.confidence >= 55;
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
return formInfo;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
classifyNewsletterForm
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
const { Command } = require('commander');
|
||||
|
||||
function createCLI() {
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('newsletter-signup')
|
||||
.description('Newsletter Signup Automation Tool')
|
||||
.version('1.0.0')
|
||||
.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
|
||||
|
||||
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
|
||||
-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
|
||||
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
|
||||
|
||||
Configuration:
|
||||
Edit config.toml to set credentials and default sites
|
||||
`);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
function parseCLI(args = process.argv.slice(2)) {
|
||||
const program = createCLI();
|
||||
|
||||
const fullArgs = ['node', 'index.js', ...args];
|
||||
program.parse(fullArgs);
|
||||
|
||||
const opts = program.opts();
|
||||
|
||||
return {
|
||||
target: program.args[0] || null,
|
||||
file: opts.file || null,
|
||||
keepOpen: opts.keepOpen || false,
|
||||
log: opts.log || false,
|
||||
dryRun: opts.dryRun || false,
|
||||
help: opts.help || false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCLI,
|
||||
parseCLI
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const toml = require('@iarna/toml');
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const configPath = path.join(__dirname, '..', 'config.toml');
|
||||
const tomlString = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = toml.parse(tomlString);
|
||||
console.log('Configuration loaded successfully.');
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('Failed to load or parse config.toml:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generateRandomData() {
|
||||
const randomString = Math.random().toString(36).substring(2, 10);
|
||||
const username = `user${randomString}`;
|
||||
const email = `${username}@gmail.com`;
|
||||
const password = `pass${randomString}`;
|
||||
const age = Math.floor(Math.random() * 30 + 20).toString();
|
||||
const firstName = 'John';
|
||||
const lastName = 'Doe';
|
||||
const phone = '+31610488717';
|
||||
const company = 'Example Corp';
|
||||
return { username, email, password, age, firstName, lastName, phone, company };
|
||||
}
|
||||
|
||||
function parseLocator(locatorStr) {
|
||||
locatorStr = locatorStr.trim();
|
||||
if (!locatorStr.startsWith('<')) {
|
||||
return locatorStr;
|
||||
}
|
||||
|
||||
const getAttr = (attr) => {
|
||||
const match = locatorStr.match(new RegExp(`${attr}=["']([^"']+)["']`));
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const tagName = (locatorStr.match(/^<([a-zA-Z0-9]+)/) || [])[1] || '';
|
||||
const id = getAttr('id');
|
||||
if (id) {
|
||||
if (/^\d/.test(id)) {
|
||||
return `${tagName}[id='${id}']`;
|
||||
}
|
||||
return `${tagName}#${id}`;
|
||||
}
|
||||
|
||||
const name = getAttr('name');
|
||||
if (name) return `${tagName}[name='${name}']`;
|
||||
|
||||
const placeholder = getAttr('placeholder');
|
||||
if (placeholder) return `${tagName}[placeholder='${placeholder}']`;
|
||||
|
||||
const type = getAttr('type');
|
||||
if (type) return `${tagName}[type='${type}']`;
|
||||
|
||||
return tagName;
|
||||
}
|
||||
|
||||
function loadSitesFromFile(filePath) {
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(__dirname, '..', filePath);
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const urls = content.split('\n').filter(line => line.trim() && !line.startsWith('#'));
|
||||
return urls.map((url, index) => ({
|
||||
name: `Site_${index + 1}`,
|
||||
url: url.trim(),
|
||||
category: 'Newsletter',
|
||||
needsPreSubmitAction: false
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadConfig,
|
||||
generateRandomData,
|
||||
parseLocator,
|
||||
loadSitesFromFile
|
||||
};
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
const FIELD_PATTERNS = {
|
||||
email: {
|
||||
priority: 1,
|
||||
types: ['email'],
|
||||
names: ['email', 'e-mail', 'mail', 'user_email', 'login_email', 'email_address', 'emailaddress'],
|
||||
ids: ['email', 'e-mail', 'mail', 'user-email', 'login-email'],
|
||||
placeholders: ['email', 'e-mail', 'email address', 'your email', 'enter email'],
|
||||
autocomplete: ['email']
|
||||
},
|
||||
confirmEmail: {
|
||||
priority: 2,
|
||||
types: ['email'],
|
||||
names: ['confirm_email', 'confirmemail', 'email_confirm', 'verify_email', 'email_verify'],
|
||||
ids: ['confirm-email', 'confirmemail', 'verify-email', 'email-confirm'],
|
||||
placeholders: ['confirm email', 'verify email', 're-enter email', 'repeat email'],
|
||||
mustHaveConfirmIndicator: true
|
||||
},
|
||||
firstName: {
|
||||
priority: 3,
|
||||
types: ['text'],
|
||||
names: ['first_name', 'firstname', 'fname', 'first-name', 'vorname', 'prenom', 'fname'],
|
||||
ids: ['first-name', 'firstname', 'fname', 'firstName', 'mce-fname'],
|
||||
placeholders: ['first name', 'firstname', 'first'],
|
||||
autocomplete: ['given-name', 'first-name']
|
||||
},
|
||||
lastName: {
|
||||
priority: 4,
|
||||
types: ['text'],
|
||||
names: ['last_name', 'lastname', 'lname', 'surname', 'family_name', 'last-name', 'lname'],
|
||||
ids: ['last-name', 'lastname', 'lname', 'surname', 'lastName', 'mce-lname'],
|
||||
placeholders: ['last name', 'lastname', 'surname', 'last'],
|
||||
autocomplete: ['family-name', 'last-name']
|
||||
},
|
||||
username: {
|
||||
priority: 5,
|
||||
types: ['text'],
|
||||
names: ['username', 'user_name', 'login', 'usr', 'uname', 'user-id', 'userid', 'account_name', 'login_id'],
|
||||
ids: ['username', 'user-name', 'login', 'usr', 'uname'],
|
||||
placeholders: ['username', 'user name', 'login', 'username or email'],
|
||||
autocomplete: ['username']
|
||||
},
|
||||
password: {
|
||||
priority: 6,
|
||||
types: ['password'],
|
||||
names: ['password', 'pass', 'pwd', 'user_password', 'login_password'],
|
||||
ids: ['password', 'pass', 'pwd'],
|
||||
placeholders: ['password', 'pass', 'enter password'],
|
||||
autocomplete: ['current-password', 'new-password']
|
||||
},
|
||||
fullName: {
|
||||
priority: 7,
|
||||
types: ['text'],
|
||||
names: ['full_name', 'fullname', 'name'],
|
||||
ids: ['full-name', 'fullname', 'name'],
|
||||
placeholders: ['full name', 'your name', 'complete name'],
|
||||
autocomplete: ['name']
|
||||
},
|
||||
phone: {
|
||||
priority: 8,
|
||||
types: ['tel', 'text'],
|
||||
names: ['phone', 'telephone', 'mobile', 'cell', 'cellphone', 'phone_number'],
|
||||
ids: ['phone', 'telephone', 'mobile', 'cell', 'tel'],
|
||||
placeholders: ['phone', 'telephone', 'mobile number'],
|
||||
autocomplete: ['tel']
|
||||
},
|
||||
age: {
|
||||
priority: 9,
|
||||
types: ['number', 'text'],
|
||||
names: ['age', 'user_age'],
|
||||
ids: ['age'],
|
||||
placeholders: ['age', 'your age']
|
||||
},
|
||||
company: {
|
||||
priority: 10,
|
||||
types: ['text'],
|
||||
names: ['company', 'organization', 'company_name', 'employer'],
|
||||
ids: ['company', 'organization', 'employer'],
|
||||
placeholders: ['company', 'organization'],
|
||||
autocomplete: ['organization']
|
||||
}
|
||||
};
|
||||
|
||||
async function detectFormFields(page) {
|
||||
console.log('Auto-detecting form fields...');
|
||||
|
||||
const detectedFields = await page.evaluate((patterns) => {
|
||||
const results = {};
|
||||
const usedElements = new Set();
|
||||
|
||||
const inputs = Array.from(document.querySelectorAll('input, select, textarea')).filter(input => {
|
||||
const type = input.type?.toLowerCase() || 'text';
|
||||
const style = window.getComputedStyle(input);
|
||||
const rect = input.getBoundingClientRect();
|
||||
|
||||
if (type === 'hidden' || type === 'submit' || type === 'button') return false;
|
||||
if (input.tabIndex === -1 && input.style.position === 'absolute') return false;
|
||||
if (rect.width === 0 || rect.height === 0) return false;
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const calculateScore = (input, pattern, fieldName) => {
|
||||
let score = 0;
|
||||
const inputType = (input.type || 'text').toLowerCase();
|
||||
const inputName = (input.name || '').toLowerCase();
|
||||
const inputId = (input.id || '').toLowerCase();
|
||||
const placeholder = (input.placeholder || '').toLowerCase();
|
||||
const autocomplete = (input.autocomplete || '').toLowerCase();
|
||||
|
||||
if (pattern.types && pattern.types.includes(inputType)) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
if (pattern.names) {
|
||||
for (const name of pattern.names) {
|
||||
const nameLower = name.toLowerCase();
|
||||
if (inputName === nameLower) {
|
||||
score += 20;
|
||||
break;
|
||||
} else if (inputName.includes(nameLower)) {
|
||||
if (fieldName === 'firstName' && (inputName.includes('user') || inputName.includes('login'))) {
|
||||
score -= 5;
|
||||
} else if (fieldName === 'lastName' && (inputName.includes('user') || inputName.includes('login'))) {
|
||||
score -= 5;
|
||||
} else {
|
||||
score += 10;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pattern.ids) {
|
||||
for (const id of pattern.ids) {
|
||||
const idLower = id.toLowerCase();
|
||||
if (inputId === idLower) {
|
||||
score += 20;
|
||||
break;
|
||||
} else if (inputId.includes(idLower)) {
|
||||
score += 10;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pattern.placeholders) {
|
||||
for (const ph of pattern.placeholders) {
|
||||
const phLower = ph.toLowerCase();
|
||||
if (placeholder === phLower) {
|
||||
score += 15;
|
||||
break;
|
||||
} else if (placeholder.includes(phLower)) {
|
||||
score += 8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pattern.autocomplete) {
|
||||
for (const ac of pattern.autocomplete) {
|
||||
if (autocomplete === ac.toLowerCase()) {
|
||||
score += 12;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const labels = input.labels;
|
||||
if (labels && labels.length > 0) {
|
||||
const labelText = labels[0].textContent.toLowerCase();
|
||||
const fieldNameLower = fieldName.toLowerCase().replace(/([A-Z])/g, ' $1').trim();
|
||||
if (labelText.includes(fieldNameLower) || labelText.includes(pattern.names?.[0] || '')) {
|
||||
score += 15;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputName.includes('b_') || inputId.includes('b_')) {
|
||||
score -= 30;
|
||||
}
|
||||
|
||||
if (input.disabled || input.readOnly) {
|
||||
score -= 20;
|
||||
}
|
||||
|
||||
if (pattern.mustHaveConfirmIndicator) {
|
||||
const hasConfirm = inputId.includes('confirm') || inputId.includes('verify') ||
|
||||
inputName.includes('confirm') || inputName.includes('verify') ||
|
||||
placeholder.includes('confirm') || placeholder.includes('verify') ||
|
||||
placeholder.includes('re-') || placeholder.includes('again');
|
||||
if (!hasConfirm) {
|
||||
score -= 15;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
};
|
||||
|
||||
const sortedPatterns = Object.entries(patterns).sort((a, b) => a[1].priority - b[1].priority);
|
||||
|
||||
for (const [fieldName, pattern] of sortedPatterns) {
|
||||
let bestMatch = null;
|
||||
let bestScore = 0;
|
||||
let bestIndex = -1;
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
if (usedElements.has(i)) continue;
|
||||
|
||||
const input = inputs[i];
|
||||
const score = calculateScore(input, pattern, fieldName);
|
||||
|
||||
if (score > bestScore && score >= 10) {
|
||||
bestScore = score;
|
||||
bestMatch = input;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch && bestIndex >= 0) {
|
||||
usedElements.add(bestIndex);
|
||||
|
||||
let selector = '';
|
||||
if (bestMatch.id) {
|
||||
selector = `#${bestMatch.id}`;
|
||||
} else if (bestMatch.name) {
|
||||
selector = `[name="${bestMatch.name}"]`;
|
||||
} else if (bestMatch.placeholder) {
|
||||
selector = `[placeholder="${bestMatch.placeholder}"]`;
|
||||
} else {
|
||||
const tagName = bestMatch.tagName.toLowerCase();
|
||||
const type = bestMatch.type ? `[type="${bestMatch.type}"]` : '';
|
||||
const allSame = Array.from(document.querySelectorAll(`${tagName}${type}`));
|
||||
const index = allSame.indexOf(bestMatch) + 1;
|
||||
selector = `${tagName}${type}:nth-of-type(${index})`;
|
||||
}
|
||||
|
||||
results[fieldName] = {
|
||||
selector,
|
||||
confidence: bestScore,
|
||||
name: bestMatch.name,
|
||||
id: bestMatch.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}, FIELD_PATTERNS);
|
||||
|
||||
const detectedCount = Object.keys(detectedFields).length;
|
||||
if (detectedCount === 0) {
|
||||
console.log(' No fields detected');
|
||||
}
|
||||
|
||||
return detectedFields;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FIELD_PATTERNS,
|
||||
detectFormFields
|
||||
};
|
||||
@@ -0,0 +1,327 @@
|
||||
const { generateRandomData, parseLocator } = require('./config');
|
||||
const { detectFormFields } = require('./fields');
|
||||
const { detectSubmitButton } = require('./submit');
|
||||
const { detectCaptcha, solveSimpleCaptcha, solveHCaptcha } = require('./captcha');
|
||||
const { classifyNewsletterForm } = require('./classifier');
|
||||
const { detectSubmissionResult } = require('./submission');
|
||||
|
||||
async function processSite(browser, site, globalCreds, browserConfig, keepOpen = false, dryRun = false) {
|
||||
let siteFailed = false;
|
||||
let failureReason = '';
|
||||
let page = null;
|
||||
|
||||
try {
|
||||
let creds;
|
||||
if (site.credentials && site.credentials.username) {
|
||||
creds = { ...globalCreds, ...site.credentials };
|
||||
} else {
|
||||
creds = { ...globalCreds };
|
||||
}
|
||||
|
||||
if (!creds.username || !creds.email || !creds.password) {
|
||||
const randomData = generateRandomData();
|
||||
creds = { ...randomData, ...creds };
|
||||
}
|
||||
|
||||
console.log(`\n${site.url}`);
|
||||
console.log(`${'-'.repeat(70)}`);
|
||||
|
||||
page = await browser.newPage();
|
||||
|
||||
if (browserConfig.userAgent) {
|
||||
await page.setUserAgent(browserConfig.userAgent);
|
||||
}
|
||||
|
||||
await page.goto(site.url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
|
||||
const captchaInfo = await detectCaptcha(page);
|
||||
if (captchaInfo.length > 0) {
|
||||
console.log(`\n CAPTCHA DETECTED:`);
|
||||
for (const captcha of captchaInfo) {
|
||||
const visibility = captcha.isVisible ? '(visible)' : '(hidden)';
|
||||
console.log(` - ${captcha.name} ${visibility}`);
|
||||
}
|
||||
|
||||
if (captchaInfo.some(c => c.type === 'mathCaptcha')) {
|
||||
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 === '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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visibleRecaptcha = captchaInfo.filter(c => (c.type === 'recaptcha' || c.type === 'hcaptcha') && c.isVisible);
|
||||
if (visibleRecaptcha.length > 0) {
|
||||
siteFailed = true;
|
||||
failureReason = `CAPTCHA present: ${visibleRecaptcha.map(c => c.name).join(', ')}`;
|
||||
console.error(` Cannot auto-solve ${visibleRecaptcha.map(c => c.name).join(', ')}`);
|
||||
return { siteFailed, failureReason, page };
|
||||
} else if (captchaInfo.some(c => c.type === 'recaptcha' || c.type === 'hcaptcha')) {
|
||||
console.log(` CAPTCHA detected but not visible - attempting to proceed anyway`);
|
||||
}
|
||||
}
|
||||
|
||||
const formClassification = await classifyNewsletterForm(page);
|
||||
console.log(`\n Form Classification: ${formClassification.isNewsletter ? 'Newsletter' : 'Not newsletter'} (${formClassification.confidence}/100 confidence)`);
|
||||
|
||||
if (formClassification.signals.positive.length > 0) {
|
||||
console.log(' Positive signals:');
|
||||
formClassification.signals.positive.forEach(signal => console.log(` + ${signal}`));
|
||||
}
|
||||
|
||||
if (formClassification.signals.negative.length > 0) {
|
||||
console.log(' Negative signals:');
|
||||
formClassification.signals.negative.forEach(signal => console.log(` - ${signal}`));
|
||||
}
|
||||
|
||||
if (!formClassification.isNewsletter && formClassification.confidence < 40) {
|
||||
siteFailed = true;
|
||||
failureReason = 'Not a newsletter signup form';
|
||||
console.error(` ${failureReason}`);
|
||||
}
|
||||
|
||||
console.log('\nDETECTED FIELDS:');
|
||||
|
||||
let fieldsToFill = {};
|
||||
let detectedFields = {};
|
||||
const definedLocators = site.locators || {};
|
||||
|
||||
if (site.autoDetect !== false) {
|
||||
detectedFields = await detectFormFields(page);
|
||||
for (const [fieldName, fieldInfo] of Object.entries(detectedFields)) {
|
||||
fieldsToFill[fieldName] = fieldInfo.selector;
|
||||
}
|
||||
}
|
||||
|
||||
const hasEmailField = fieldsToFill.email || fieldsToFill.confirmEmail;
|
||||
if (!hasEmailField) {
|
||||
siteFailed = true;
|
||||
failureReason = 'No email field detected';
|
||||
console.warn(`\n ${failureReason}`);
|
||||
}
|
||||
|
||||
const hasSearchInput = await page.evaluate(() => {
|
||||
const inputs = document.querySelectorAll('input[type="search"], input[name*="search"], input[id*="search"]');
|
||||
return inputs.length > 0;
|
||||
}).catch(() => false);
|
||||
|
||||
if (hasSearchInput && !hasEmailField) {
|
||||
console.warn(` Search form detected instead of newsletter`);
|
||||
siteFailed = true;
|
||||
failureReason = failureReason || 'Search form instead of newsletter';
|
||||
}
|
||||
|
||||
for (const [fieldName, locator] of Object.entries(definedLocators)) {
|
||||
if (fieldName !== 'submit') {
|
||||
fieldsToFill[fieldName] = parseLocator(locator);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(fieldsToFill).length > 0) {
|
||||
console.log(`\n ${'FIELD'.padEnd(12)} | ${'SELECTOR'.padEnd(35)} | ${'CONF'.padEnd(4)} | VALUE`);
|
||||
console.log(' ' + '-'.repeat(75));
|
||||
|
||||
for (const [fieldName, fieldInfo] of Object.entries(detectedFields)) {
|
||||
let valueToFill = creds[fieldName];
|
||||
|
||||
if (fieldName === 'confirmEmail') {
|
||||
valueToFill = creds['email'];
|
||||
}
|
||||
|
||||
const selector = fieldInfo.selector.length > 34 ? fieldInfo.selector.substring(0, 31) + '...' : fieldInfo.selector;
|
||||
const value = valueToFill ? valueToFill.substring(0, 20) : 'N/A';
|
||||
|
||||
console.log(` ${fieldName.padEnd(12)} | ${selector.padEnd(35)} | ${fieldInfo.confidence.toString().padEnd(4)} | ${value}`);
|
||||
|
||||
if (valueToFill) {
|
||||
try {
|
||||
await page.evaluate((sel, val) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) {
|
||||
el.value = val;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}, fieldInfo.selector, valueToFill);
|
||||
} catch (e) {
|
||||
console.warn(` Failed to fill ${fieldName}: ${e.message}`);
|
||||
siteFailed = true;
|
||||
failureReason = `Failed to fill ${fieldName}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(' ' + '-'.repeat(75));
|
||||
}
|
||||
|
||||
let submitSelector = null;
|
||||
if (definedLocators.submit) {
|
||||
submitSelector = parseLocator(definedLocators.submit);
|
||||
} else if (site.autoDetect !== false) {
|
||||
submitSelector = await detectSubmitButton(page);
|
||||
}
|
||||
|
||||
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, urlBeforeSubmit);
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hCaptcha on redirected pages (like /challenge)
|
||||
const postSubmitCaptcha = await detectCaptcha(page);
|
||||
const visibleHCaptcha = postSubmitCaptcha.filter(c => c.type === 'hcaptcha' && c.isVisible);
|
||||
const hasChallengeRedirect = page.url().includes('/challenge') || page.url().includes('captcha');
|
||||
|
||||
if ((visibleHCaptcha.length > 0 || hasChallengeRedirect) && !dryRun) {
|
||||
console.log(` hCaptcha/challenge detected - attempting to solve...`);
|
||||
const hcaptchaResult = await solveHCaptcha(page, page.url());
|
||||
|
||||
if (hcaptchaResult && hcaptchaResult.answer) {
|
||||
console.log(` hCaptcha solved - resubmitting form...`);
|
||||
|
||||
// Wait a moment for the token to be set
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
const resubmitSelector = await detectSubmitButton(page);
|
||||
if (resubmitSelector) {
|
||||
await page.click(resubmitSelector);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
const resubmitResult = await detectSubmissionResult(page, urlBeforeSubmit);
|
||||
if (resubmitResult.success) {
|
||||
console.log(` SUCCESS after hCaptcha solve!`);
|
||||
siteFailed = false;
|
||||
failureReason = '';
|
||||
} else if (resubmitResult.failure) {
|
||||
siteFailed = true;
|
||||
failureReason = resubmitResult.failureReason || 'Failed after hCaptcha';
|
||||
} else {
|
||||
// Still unknown but hCaptcha was solved - assume success
|
||||
console.log(` hCaptcha solved, result unknown - assuming success`);
|
||||
siteFailed = false;
|
||||
failureReason = '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(` Could not solve hCaptcha`);
|
||||
siteFailed = true;
|
||||
failureReason = 'hCaptcha solving failed';
|
||||
}
|
||||
} else if ((visibleHCaptcha.length > 0 || hasChallengeRedirect) && dryRun) {
|
||||
console.log(` [DRY-RUN] Would solve hCaptcha/challenge`);
|
||||
} else if (submissionResult.failure) {
|
||||
if (siteFailed) {
|
||||
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}`;
|
||||
console.error(` Submit failed: ${submitError.message}`);
|
||||
}
|
||||
} else {
|
||||
siteFailed = true;
|
||||
failureReason = failureReason || 'No submit button found';
|
||||
console.warn(` ${failureReason}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
siteFailed = true;
|
||||
failureReason = error.message;
|
||||
console.error(` Error: ${error.message}`);
|
||||
} finally {
|
||||
if (page && !keepOpen) {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
return { siteFailed, failureReason, page };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processSite
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
async function detectSubmissionResult(page, urlBeforeSubmit = null) {
|
||||
const result = await page.evaluate((beforeUrl) => {
|
||||
const results = {
|
||||
success: false,
|
||||
failure: false,
|
||||
failureReason: null,
|
||||
currentUrl: window.location.href,
|
||||
signals: []
|
||||
};
|
||||
|
||||
// Check if URL changed significantly (often indicates success)
|
||||
if (beforeUrl && window.location.href !== beforeUrl) {
|
||||
results.signals.push(`URL changed from ${beforeUrl.split('?')[0]} to ${window.location.href.split('?')[0]}`);
|
||||
}
|
||||
|
||||
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',
|
||||
'almost there', 'confirm your subscription', 'verify your email'
|
||||
];
|
||||
|
||||
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', 'email already exists',
|
||||
'you have already subscribed', 'already registered'
|
||||
];
|
||||
|
||||
if (!results.success) {
|
||||
for (const keyword of failureKeywords) {
|
||||
if (pageText.includes(keyword)) {
|
||||
results.failure = true;
|
||||
results.failureReason = `Failure keyword detected: "${keyword}"`;
|
||||
results.signals.push(`Failure: ${keyword}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only check form visibility if we haven't determined success/failure yet
|
||||
if (!results.success && !results.failure) {
|
||||
const formStillVisible = document.querySelector('form') !== null;
|
||||
const emailInputStillVisible = document.querySelector('input[type="email"]') !== null ||
|
||||
document.querySelector('input[name*="email" i]') !== null;
|
||||
|
||||
if (formStillVisible && emailInputStillVisible) {
|
||||
// Form still visible but no explicit success/failure - could be either
|
||||
results.signals.push('Form still visible but no explicit result - unknown');
|
||||
} else if (!formStillVisible && !emailInputStillVisible) {
|
||||
// Form disappeared - likely success
|
||||
results.success = true;
|
||||
results.signals.push('Form no longer visible - assuming success');
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}, urlBeforeSubmit);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectSubmissionResult
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
async function detectSubmitButton(page) {
|
||||
console.log('Auto-detecting submit button...');
|
||||
|
||||
const submitSelector = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"], button, input[type="button"]'));
|
||||
let bestMatch = null;
|
||||
let bestScore = 0;
|
||||
|
||||
buttons.forEach(btn => {
|
||||
const text = (btn.textContent || btn.value || '').toLowerCase().trim();
|
||||
const type = btn.type?.toLowerCase() || '';
|
||||
const className = (btn.className || '').toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
if (type === 'submit') score += 20;
|
||||
|
||||
if (text.includes('subscribe')) score += 15;
|
||||
else if (text.includes('sign up')) score += 12;
|
||||
else if (text.includes('submit')) score += 10;
|
||||
else if (text.includes('join')) score += 8;
|
||||
else if (text.includes('register')) score += 8;
|
||||
else if (text.includes('send')) score += 5;
|
||||
|
||||
if (className.includes('submit')) score += 8;
|
||||
if (className.includes('subscribe')) score += 8;
|
||||
|
||||
if (className.includes('close') || text.includes('close')) score -= 20;
|
||||
if (className.includes('cancel') || text.includes('cancel')) score -= 20;
|
||||
if (className.includes('back') || text.includes('back')) score -= 15;
|
||||
if (className.includes('drawer')) score -= 15;
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0 && rect.top > 0) {
|
||||
score += 3;
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestMatch = btn;
|
||||
}
|
||||
});
|
||||
|
||||
if (bestMatch && bestScore >= 10) {
|
||||
if (bestMatch.id) return `#${bestMatch.id}`;
|
||||
if (bestMatch.name) return `[name="${bestMatch.name}"]`;
|
||||
|
||||
const className = bestMatch.className;
|
||||
if (className) {
|
||||
const classes = className.split(' ').filter(c => c.length > 0);
|
||||
for (const cls of classes) {
|
||||
const clsLower = cls.toLowerCase();
|
||||
if (clsLower.includes('submit') || clsLower.includes('subscribe')) {
|
||||
return `.${cls}`;
|
||||
}
|
||||
}
|
||||
for (const cls of classes) {
|
||||
if (!cls.toLowerCase().includes('close') && !cls.toLowerCase().includes('cancel')) {
|
||||
return `.${cls}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allBtns = Array.from(document.querySelectorAll('button'));
|
||||
const index = allBtns.indexOf(bestMatch) + 1;
|
||||
return `button:nth-of-type(${index})`;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if (submitSelector) {
|
||||
console.log(` Submit button found: ${submitSelector}`);
|
||||
} else {
|
||||
console.log(' No submit button found');
|
||||
}
|
||||
|
||||
return submitSelector;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectSubmitButton
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
https://immaculatevegan.com/blogs/magazine
|
||||
https://gate6.vn/
|
||||
https://www.sprintzeal.com/
|
||||
https://kulalaland.com/
|
||||
https://www.goplaces.co.uk/
|
||||
https://www.fiftyfiftybottles.com/
|
||||
https://jubina.com/blog
|
||||
https://www.sneakywholefoods.com/
|
||||
https://vegnews.com/signup
|
||||
https://yonayonabeerworks.com/
|
||||
https://www.louispowersportsonline.com/
|
||||
https://www.hairworthy.co.uk/
|
||||
https://endotaspa.com.au/
|
||||
https://prestigify.com/
|
||||
https://nobones.co/
|
||||
https://www.sofamania.com/
|
||||
https://dimoradellebalze.com/en/restaurant/
|
||||
https://www.42photo.com/
|
||||
https://ifinishedmybasement.com/
|
||||
https://tettixgames.com/
|
||||
https://www.corinnekai.com/raw-honey-raw-self-newsletter/
|
||||
https://www.blendnwhip.com/
|
||||
https://www.brookebowlin.com/nuance-required
|
||||
https://www.thatvegandad.net/
|
||||
https://comhwa.org.au/blog/newsletters/
|
||||
https://hiutdenim.co.uk/
|
||||
https://local-lovely.com/
|
||||
https://vituswholefoods.com/
|
||||
https://theoriginalshabby.it/
|
||||
https://rothys.com/en-au
|
||||
https://www.trustbasket.com/
|
||||
https://chelseabrice.com/
|
||||
https://wolfsgamingblog.com/
|
||||
https://www.leadfuze.com/blog/
|
||||
https://thedefiant.io/
|
||||
https://www.entrepreneur.com/
|
||||
https://pantproject.com/
|
||||
https://www.terrebleu.ca/
|
||||
https://www.shopify.com/in/blog/shopify-stores
|
||||
https://www.skooob.com/
|
||||
https://mremountain.com/
|
||||
https://uncommonjames.com/
|
||||
https://www.axios.com/signup/tampa-bay
|
||||
https://www.dietassassinista.com/blog/tag/African+Cuisine
|
||||
https://miriamporter.ca/
|
||||
https://uppercasemagazine.com/
|
||||
https://www.mercadodasrosas.com/
|
||||
https://mybageecha.com/
|
||||
https://bebemoss.com/
|
||||
https://linkwithin.com/
|
||||
Reference in New Issue
Block a user