initial commit

This commit is contained in:
2026-06-18 10:53:32 +02:00
commit 6a44090153
20 changed files with 5524 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
+88
View File
@@ -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
View File
@@ -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
BIN
View File
Binary file not shown.
+7
View File
@@ -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
+146
View File
@@ -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();
+354
View File
@@ -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/
+2114
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -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"
}
}
+87
View File
@@ -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
View File
@@ -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
};
+109
View File
@@ -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
View File
@@ -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
};
+80
View File
@@ -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
View File
@@ -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
};
+327
View File
@@ -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
};
+110
View File
@@ -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
};
+82
View File
@@ -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
View File
@@ -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/