first commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* 9ROUTER-ANTIGRAVITY AUTO ADD BOT
|
||||
*
|
||||
* Uses CloakBrowser (stealth Chromium) to bypass bot detection
|
||||
* Accounts loaded from accounts.txt (format: email|password per line)
|
||||
*
|
||||
* Usage: node bot.js [--headless] [--dry-run] [--file accounts2.txt]
|
||||
*/
|
||||
|
||||
import { launch, getDefaultStealthArgs, ensureBinary } from 'cloakbrowser';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
||||
const args = process.argv.slice(2);
|
||||
const HEADLESS = args.includes('--headless') || undefined;
|
||||
const DRY_RUN = args.includes('--dry-run');
|
||||
|
||||
const fileArgIndex = args.indexOf('--file');
|
||||
const CUSTOM_FILE = fileArgIndex !== -1 ? args[fileArgIndex + 1] : null;
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
console.error(`[X] Config not found: ${CONFIG_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
|
||||
function loadAccounts(config) {
|
||||
const fileName = CUSTOM_FILE || config.accountsFile || 'accounts.txt';
|
||||
const filePath = path.resolve(__dirname, fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[X] Accounts file not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(filePath, 'utf-8')
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l && !l.startsWith('#'));
|
||||
|
||||
const accounts = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split('|');
|
||||
if (parts.length < 2) {
|
||||
console.warn(`[!] Skipping invalid line: ${line}`);
|
||||
continue;
|
||||
}
|
||||
accounts.push({ email: parts[0].trim(), password: parts[1].trim() });
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const dir = path.join(__dirname, 'screenshots');
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, `${name}-${Date.now()}.png`);
|
||||
await page.screenshot({ path: file, fullPage: true });
|
||||
console.log(` [IMG] Screenshot: ${file}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
// Try multiple selectors, click first one that's visible
|
||||
async function clickAny(page, selectors, label, timeout = 5000) {
|
||||
for (const sel of selectors) {
|
||||
const el = page.getByRole('button', { name: sel });
|
||||
const visible = await el.isVisible({ timeout }).catch(() => false);
|
||||
if (visible) {
|
||||
console.log(` [>] ${label} → "${sel}"`);
|
||||
await el.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try multiple textbox labels, fill first one that's visible
|
||||
async function fillAny(page, labels, value, label, timeout = 10000) {
|
||||
for (const lbl of labels) {
|
||||
const el = page.getByRole('textbox', { name: lbl });
|
||||
const visible = await el.isVisible({ timeout: Math.min(timeout, 3000) }).catch(() => false);
|
||||
if (visible) {
|
||||
console.log(` [>] ${label} → "${lbl}"`);
|
||||
await el.click();
|
||||
await el.fill(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function addAccount(browser, account, index, total, config) {
|
||||
const baseUrl = config.nineRouter.url;
|
||||
console.log(`\n[ADD] [${index}/${total}] ${account.email}`);
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log(' [~] DRY RUN - skipping');
|
||||
return { success: true, email: account.email, action: 'dry-run' };
|
||||
}
|
||||
|
||||
// Fresh context per akun — clean session, forced English locale
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 },
|
||||
locale: 'en-US',
|
||||
extraHTTPHeaders: { 'Accept-Language': 'en-US,en;q=0.9' }
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(config.settings?.timeout || 60000);
|
||||
|
||||
try {
|
||||
// Login 9router fresh setiap akun
|
||||
console.log(' [>] Login 9router...');
|
||||
await page.goto(`${baseUrl}/login`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByRole('textbox', { name: 'Enter password' }).click();
|
||||
await page.getByRole('textbox', { name: 'Enter password' }).fill(config.nineRouter.password);
|
||||
await page.getByRole('textbox', { name: 'Enter password' }).press('Enter');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Navigate ke Antigravity
|
||||
console.log(' [>] Navigate to Antigravity...');
|
||||
await page.getByRole('link', { name: 'dns Providers' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByRole('link', { name: /Antigravity/ }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click Add button
|
||||
await page.getByRole('button', { name: 'add Add', exact: true }).click();
|
||||
await sleep(1000);
|
||||
|
||||
// Click "I Understand, Continue" - triggers Google OAuth popup
|
||||
const popupPromise = page.waitForEvent('popup', { timeout: 15000 });
|
||||
await clickAny(page, [
|
||||
'I Understand, Continue',
|
||||
'I understand, continue',
|
||||
/understand/i,
|
||||
], 'I Understand');
|
||||
|
||||
const popup = await popupPromise;
|
||||
popup.setDefaultTimeout(60000);
|
||||
await popup.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Force English UI on Google OAuth page
|
||||
const popupUrl = new URL(popup.url());
|
||||
popupUrl.searchParams.set('hl', 'en');
|
||||
await popup.goto(popupUrl.toString());
|
||||
await popup.waitForLoadState('networkidle');
|
||||
await sleep(2000);
|
||||
|
||||
// Google Sign-in - Email
|
||||
console.log(' [>] Email...');
|
||||
const emailFilled = await fillAny(popup, [
|
||||
'Email or phone',
|
||||
'Enter your email',
|
||||
'Email',
|
||||
'Alamat email atau nomor telepon',
|
||||
'E-mail atau telepon',
|
||||
'メールアドレスまたは電話番号',
|
||||
'이메일 또는 전화번호',
|
||||
'电子邮件或电话号码',
|
||||
'E-mail ou téléphone',
|
||||
'Correo electrónico o teléfono',
|
||||
'E-Mail oder Telefon',
|
||||
'Электронная почта или телефон',
|
||||
], account.email, 'Email');
|
||||
if (!emailFilled) throw new Error('Could not find email input field');
|
||||
|
||||
await clickAny(popup, [
|
||||
'Next',
|
||||
'Berikutnya',
|
||||
'Lanjut',
|
||||
'次へ',
|
||||
'다음',
|
||||
'下一步',
|
||||
'Suivant',
|
||||
'Siguiente',
|
||||
'Weiter',
|
||||
'Далее',
|
||||
], 'Next', 5000);
|
||||
await sleep(3000);
|
||||
|
||||
// Google Sign-in - Password
|
||||
console.log(' [>] Password...');
|
||||
const pwFilled = await fillAny(popup, [
|
||||
'Enter your password',
|
||||
'Enter password',
|
||||
'Password',
|
||||
'Masukkan kata sandi Anda',
|
||||
'Kata sandi',
|
||||
'パスワードを入力',
|
||||
'비밀번호 입력',
|
||||
'输入密码',
|
||||
'Entrez votre mot de passe',
|
||||
'Introduce tu contraseña',
|
||||
'Geben Sie Ihr Passwort ein',
|
||||
'Введите пароль',
|
||||
], account.password, 'Password');
|
||||
if (!pwFilled) throw new Error('Could not find password input field');
|
||||
await popup.keyboard.press('Enter');
|
||||
await sleep(3000);
|
||||
|
||||
// Verify: Workspace Terms - click if exists, skip if not
|
||||
const hasTerms = await clickAny(popup, [
|
||||
'I understand',
|
||||
'Saya mengerti',
|
||||
'理解しました',
|
||||
'이해했습니다',
|
||||
'我明白了',
|
||||
'Je comprends',
|
||||
'Entiendo',
|
||||
'Ich verstehe',
|
||||
'Я понимаю',
|
||||
], 'Workspace Terms', 3000);
|
||||
if (hasTerms) {
|
||||
await popup.waitForLoadState('networkidle');
|
||||
await sleep(3000);
|
||||
} else {
|
||||
console.log(' [~] No Terms page, skipping');
|
||||
}
|
||||
|
||||
// Handle OAuth consent in popup (MUST click before popup closes)
|
||||
console.log(' [>] Waiting for OAuth consent page...');
|
||||
try {
|
||||
await popup.waitForURL(/oauth\/consent|nativeapp/, { timeout: 30000 });
|
||||
await popup.waitForLoadState('networkidle');
|
||||
await sleep(2000);
|
||||
|
||||
const consentClicked = await clickAny(popup, [
|
||||
'Sign in',
|
||||
'Continue',
|
||||
'Allow',
|
||||
'Login',
|
||||
'Masuk',
|
||||
'Lanjutkan',
|
||||
'Izinkan',
|
||||
'ログイン',
|
||||
'続行',
|
||||
'許可',
|
||||
'로그인',
|
||||
'계속',
|
||||
'허용',
|
||||
'登录',
|
||||
'继续',
|
||||
'允许',
|
||||
'Se connecter',
|
||||
'Continuer',
|
||||
'Autoriser',
|
||||
'Iniciar sesión',
|
||||
'Continuar',
|
||||
'Permitir',
|
||||
'Anmelden',
|
||||
'Fortfahren',
|
||||
'Zulassen',
|
||||
'Войти',
|
||||
'Продолжить',
|
||||
'Разрешить',
|
||||
], 'OAuth Consent', 15000);
|
||||
if (!consentClicked) throw new Error('Could not find consent button');
|
||||
|
||||
await sleep(5000);
|
||||
} catch (err) {
|
||||
console.log(` [!] Consent step: ${err.message}`);
|
||||
}
|
||||
|
||||
// Popup auto-closes after consent
|
||||
await sleep(2000);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify: refresh Antigravity page and check if email appears
|
||||
console.log(' [>] Verifying...');
|
||||
await page.goto(`${baseUrl}/dashboard/providers/antigravity`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await sleep(3000);
|
||||
|
||||
let verified = false;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
const pageText = await page.textContent('body');
|
||||
if (pageText.includes(account.email)) {
|
||||
console.log(` [OK] Verified: ${account.email} found on page`);
|
||||
verified = true;
|
||||
break;
|
||||
}
|
||||
if (attempt < 3) {
|
||||
console.log(` [~] Not found yet, refresh ${attempt}/3...`);
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await sleep(3000);
|
||||
}
|
||||
}
|
||||
if (!verified) {
|
||||
console.log(` [!] Warning: ${account.email} NOT found after 3 refreshes`);
|
||||
throw new Error('Account not verified on 9router page after OAuth');
|
||||
}
|
||||
|
||||
// Check for backend error
|
||||
const pageContent = await page.content();
|
||||
if (pageContent.includes('Failed to assign a backend')) {
|
||||
throw new Error('Failed to assign a backend - account may already exist or quota exceeded');
|
||||
}
|
||||
|
||||
console.log(` [OK] Done: ${account.email}`);
|
||||
return { success: true, email: account.email };
|
||||
|
||||
} catch (err) {
|
||||
console.error(` [X] Failed: ${err.message}`);
|
||||
try { await screenshot(page, `error-${account.email.replace(/[@.]/g, '_')}`); } catch {}
|
||||
return { success: false, email: account.email, error: err.message };
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('========================================');
|
||||
console.log(' 9ROUTER-ANTIGRAVITY AUTO ADD BOT');
|
||||
console.log(' [CloakBrowser Stealth Mode]');
|
||||
console.log('========================================');
|
||||
|
||||
// Ensure CloakBrowser binary is ready
|
||||
console.log('[*] Checking CloakBrowser binary...');
|
||||
await ensureBinary();
|
||||
|
||||
const config = loadConfig();
|
||||
const accounts = loadAccounts(config);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
console.error('[X] No accounts found. Format: email|password (one per line)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const isHeadless = HEADLESS ?? config.settings?.headless ?? false;
|
||||
|
||||
console.log(`[*] Accounts: ${accounts.length}`);
|
||||
console.log(`[*] File: ${CUSTOM_FILE || config.accountsFile || 'accounts.txt'}`);
|
||||
console.log(`[*] Headless: ${isHeadless}`);
|
||||
console.log(`[*] Dry Run: ${DRY_RUN}`);
|
||||
console.log(`[*] Target: ${config.nineRouter.url}`);
|
||||
|
||||
if (DRY_RUN) console.log('\n[!] DRY RUN MODE\n');
|
||||
|
||||
// Launch CloakBrowser with stealth args
|
||||
const browser = await launch({
|
||||
headless: isHeadless,
|
||||
slowMo: config.settings?.slowMo || 0,
|
||||
args: getDefaultStealthArgs()
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
try {
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const result = await addAccount(browser, accounts[i], i + 1, accounts.length, config);
|
||||
results.push(result);
|
||||
|
||||
if (i < accounts.length - 1) {
|
||||
const delay = config.settings?.delayBetweenAccounts || 5000;
|
||||
console.log(` [~] Waiting ${(delay / 1000).toFixed(1)}s...`);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`\n[FATAL] ${err.message}`);
|
||||
} finally {
|
||||
const success = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log(' RESULTS');
|
||||
console.log('========================================');
|
||||
console.log(`[OK] Success: ${success}/${results.length}`);
|
||||
if (failed > 0) {
|
||||
console.log(`[X] Failed: ${failed}/${results.length}`);
|
||||
console.log('\nFailed:');
|
||||
results.filter(r => !r.success).forEach(r => console.log(` - ${r.email}: ${r.error}`));
|
||||
}
|
||||
|
||||
// Remove successfully added accounts from file
|
||||
if (success > 0 && !DRY_RUN) {
|
||||
const successEmails = new Set(results.filter(r => r.success).map(r => r.email));
|
||||
const remaining = accounts.filter(a => !successEmails.has(a.email));
|
||||
const remainingLines = remaining.map(a => `${a.email}|${a.password}`);
|
||||
const accountsFile = path.resolve(__dirname, CUSTOM_FILE || config.accountsFile || 'accounts.txt');
|
||||
fs.writeFileSync(accountsFile, remainingLines.join('\n') + (remainingLines.length ? '\n' : ''));
|
||||
console.log(`\n[FILE] Updated ${accountsFile} - removed ${success} done, ${remaining.length} remaining`);
|
||||
}
|
||||
|
||||
// Save log
|
||||
const logFile = path.join(__dirname, `log-${new Date().toISOString().slice(0, 10)}.json`);
|
||||
fs.writeFileSync(logFile, JSON.stringify({ timestamp: new Date().toISOString(), results }, null, 2));
|
||||
console.log(`[LOG] ${logFile}`);
|
||||
|
||||
await browser.close();
|
||||
console.log('\n[DONE]');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[FATAL]', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user