Files
gsuite-auto/bot.js
T
2026-06-29 22:21:33 +07:00

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);
});