413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|