Files
2026-07-05 19:49:37 +07:00

422 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* Regis loop: register → create API key → fill Ultraspeed form → get referral code.
* Skips the "redeem invite code" step (entering code ref in balance page).
*
* Cara pakai:
* node regis-loop.js --count 5
*
* Output (regis-result.txt) format:
* email:password:refCode:apiKey
*/
import { chromium } from 'playwright';
import { readFileSync, appendFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { MimoRegistration, parseProxy, loadProxyList } from './index.js';
import './extras.js'; // attach getReferralCode
import { generateFingerprint, buildInitScript, buildExtraHeaders } from './fingerprint.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Output dir override via env var
const outputDir = process.env.HERMES_BOT_MIMO_CWD || __dirname;
// Config path override
const configPath = process.env.HERMES_BOT_MIMO_CONFIG || join(__dirname, 'config.json');
const config = JSON.parse(readFileSync(configPath, 'utf8'));
// ---- CLI args ---------------------------------------------------------
const args = process.argv.slice(2);
const countIdx = args.indexOf('--count');
const count = countIdx !== -1 ? parseInt(args[countIdx + 1], 10) : 1;
const outputIdx = args.indexOf('--output');
const outputFile = outputIdx !== -1
? join(outputDir, args[outputIdx + 1])
: join(outputDir, 'regis-result.txt');
const failLog = join(outputDir, 'regis-fail.log');
const proxyIdx = args.indexOf('--proxy');
let proxyList = [];
if (proxyIdx !== -1) {
const nextArg = args[proxyIdx + 1];
let proxyPath = 'proxy.txt';
if (nextArg && !nextArg.startsWith('-')) {
proxyPath = nextArg;
}
const resolvedPath = join(__dirname, proxyPath);
if (existsSync(resolvedPath)) {
proxyList = loadProxyList(resolvedPath);
console.log(`Loaded ${proxyList.length} proxies from ${proxyPath}`);
} else if (nextArg && !nextArg.startsWith('-')) {
proxyList = [nextArg];
} else {
console.log(`Proxy option enabled but ${proxyPath} not found.`);
}
} else if (config.proxy) {
proxyList = [config.proxy];
}
// ---- File helpers -----------------------------------------------------
function saveResult(row) {
if (!existsSync(outputFile)) {
appendFileSync(
outputFile,
'# Regis loop results. Format: email:password:refCode:apiKey\n',
'utf8'
);
}
const line = [
row.email,
row.password,
row.refCode || '',
row.apiKey || '',
].join(':') + '\n';
appendFileSync(outputFile, line, 'utf8');
}
function logFail(email, error) {
const line = `[${new Date().toISOString()}] ${email || 'unknown'} | ${error}\n`;
appendFileSync(failLog, line, 'utf8');
}
// ---- One-iteration runner --------------------------------------------
async function runIteration(idx, total) {
console.log('\n' + '═'.repeat(70));
console.log(` [${idx + 1}/${total}] Regis Iteration`);
console.log('═'.repeat(70));
// Per-iter: clone config + set referral link to direct URL (no referral parameter)
const iterConfig = JSON.parse(JSON.stringify(config));
if (proxyList && proxyList.length > 0) {
const selectedProxy = proxyList[idx % proxyList.length];
iterConfig.proxy = selectedProxy;
const cleanProxyName = selectedProxy.split('@').pop();
console.log(` ↳ Using proxy [${(idx % proxyList.length) + 1}/${proxyList.length}]: ${cleanProxyName}`);
}
iterConfig.xiaomi.inviteCode = '';
iterConfig.xiaomi.referralLink = 'https://platform.xiaomimimo.com/';
const reg = new MimoRegistration(iterConfig);
const cleanup = async () => {
if (reg.browser) {
console.log('\n Caught SIGINT, closing browser...');
try { await reg.browser.close(); } catch (e) {}
}
process.exit(130);
};
process.once('SIGINT', cleanup);
process.once('SIGTERM', cleanup);
let email = null, refCode = null, apiKey = null;
try {
// 1. Tempmail
console.log('[1/8] Creating temporary email...');
email = await reg.tempmail.createInbox();
console.log(`✓ Email: ${email}`);
// 2. Browser
console.log('[2/8] Launching browser...');
const fp = generateFingerprint();
reg.fingerprint = fp;
console.log(` ↳ Chrome ${fp.chromeMajor}, ${fp.viewport.width}x${fp.viewport.height}, ${fp.locale}/${fp.timezone}`);
reg.browser = await chromium.launch({
headless: iterConfig.browser.headless,
channel: 'chrome',
proxy: parseProxy(iterConfig.proxy),
args: [
`--window-size=${fp.viewport.width},${fp.viewport.height}`,
'--disable-blink-features=AutomationControlled',
],
});
const ctx = await reg.browser.newContext({
userAgent: fp.userAgent,
viewport: fp.viewport,
deviceScaleFactor: fp.deviceScaleFactor,
locale: fp.locale,
timezoneId: fp.timezone,
screen: { width: fp.screen.width, height: fp.screen.height },
extraHTTPHeaders: buildExtraHeaders(fp),
});
await ctx.addInitScript({ content: buildInitScript(fp) });
reg.page = await ctx.newPage();
// Skip screenshot dump unless config.browser.screenshots is true
const origScreenshot = reg.page.screenshot.bind(reg.page);
reg.page.screenshot = async (opts = {}) => {
const isError = opts.path && opts.path.includes('error');
if (isError || iterConfig.browser.screenshots === true) return origScreenshot(opts);
return Buffer.alloc(0);
};
// 3. Open registration page directly
console.log('[3/8] Opening registration page...');
await reg.page.goto(iterConfig.xiaomi.referralLink, {
waitUntil: 'networkidle',
timeout: iterConfig.browser.timeout,
});
// 4. Sign up form
console.log('[4/8] Filling Sign up form...');
await reg.fillRegistrationForm(email);
await reg.submitRegistration();
// 5. Captcha
console.log('[5/8] Handling captcha...');
await reg.handleXiaomiCaptcha();
await reg.handleImageCaptcha();
// 6. Verify email
console.log('[6/8] Verifying email...');
await reg.verifyEmail(email);
// 7. Check Balance (Terms modal will be handled here if opened)
console.log('[7/8] Checking balance...');
try {
// 1. Klik menu "Console" dulu — biar navigasi-nya mirip user beneran
await reg.clickConsoleMenu();
// 2. Navigasi ke balance page jika belum
let url = reg.page.url();
if (!url.includes('/console/balance')) {
console.log(` Not on balance page yet (current: ${url}), navigating...`);
const balanceClicked = await reg.page.evaluate(() => {
const links = Array.from(document.querySelectorAll('a, [role="link"], li, [role="menuitem"]'));
const target = links.find(el => {
const txt = (el.textContent || '').trim();
return /^Balance$/i.test(txt) && el.offsetHeight > 0;
});
if (target) {
target.scrollIntoView({ block: 'center' });
target.click();
return true;
}
return false;
});
if (balanceClicked) {
console.log(' ✓ Clicked Balance link in console sidebar');
await reg.page.waitForURL(/\/console\/balance/, { timeout: 8000 }).catch(() => {});
await reg.page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
} else {
console.log(' ! Balance link not found, falling back to direct goto...');
await reg.page.goto('https://platform.xiaomimimo.com/console/balance', {
waitUntil: 'networkidle',
timeout: iterConfig.browser.timeout,
});
}
}
await reg.page.waitForTimeout(2500);
await reg.handleOAuthRedirect();
await reg.page.waitForTimeout(2000);
// Close cookies banner if any
console.log(' Checking for cookies banner...');
const acceptCookiesBtn = await reg.page.waitForSelector('button:has-text("Accept All"), button:has-text("Accept"), button:has-text("Allow All")', { timeout: 4000 }).catch(() => null);
if (acceptCookiesBtn) {
await acceptCookiesBtn.click({ force: true }).catch(() => {});
console.log(' ✓ Accepted cookies banner');
await reg.page.waitForTimeout(2000);
}
// Terms modal
await reg.handleTermsModal();
// Overlays
await reg.waitForOverlaysGone();
// Take screenshot
await reg.page.screenshot({ path: 'screenshot-balance.png', fullPage: false });
console.log(' ✓ Captured screenshot-balance.png');
// Cek apakah ada notifikasi risk control / block/ restricted
const restrictionMsg = await reg.page.evaluate(() => {
const text = document.body.innerText || '';
const patterns = [
/risk\s*control\s*restriction/i,
/account\s+has\s+risk\s+control/i,
/contact\s+customer\s+service/i,
/account\s+(is\s+)?restricted/i,
];
for (const re of patterns) {
const m = text.match(new RegExp('([^\\n]{0,200}' + re.source + '[^\\n]{0,200})', re.flags));
if (m) return m[1].trim();
}
return null;
}).catch(() => null);
if (restrictionMsg) {
console.log(' ❌ ACCOUNT RESTRICTED:');
console.log(` ${restrictionMsg}`);
try {
await reg.page.screenshot({
path: `error-restriction-${Date.now()}.png`,
fullPage: true,
});
} catch (e) {}
const err = new Error(`ACCOUNT_RESTRICTED: ${restrictionMsg}`);
err.code = 'ACCOUNT_RESTRICTED';
err.restrictionMsg = restrictionMsg;
throw err;
}
// Read balance
const balance = await reg.readBalance();
console.log(` 💰 Balance: $${balance !== null ? balance.toFixed(2) : 'unknown'}`);
if (balance === 0) {
const err = new Error('Balance is $0.00');
err.code = 'BALANCE_ZERO';
throw err;
}
} catch (e) {
if (e.code === 'ACCOUNT_RESTRICTED' || e.code === 'BALANCE_ZERO') {
throw e;
}
console.log(` ! check balance error (lanjut): ${e.message}`);
}
// 8. Create API key (atau ambil existing)
console.log('[8/8] Creating API key...');
try {
apiKey = await reg.createApiKey();
console.log(`✓ API key: ${apiKey || '(none)'}`);
} catch (e) {
console.log(` ! createApiKey error: ${e.message}`);
}
// 9. Fill Ultraspeed form (best-effort)
console.log('[Bonus] Fill Ultraspeed form...');
try {
await reg.fillUltraspeedForm(email);
} catch (e) {
console.log(` ! fillUltraspeedForm error: ${e.message}`);
}
// 10. Ambil ref code akun ini PALING AKHIR — biar clipboard clean dari
// side-effect step apikey/ultraspeed sebelumnya.
console.log('[Bonus] Fetching referral code...');
try {
const captured = await reg.getReferralCode();
refCode = captured;
console.log(`✓ My ref code: ${refCode || '(not found)'}`);
} catch (e) {
console.log(` ! getReferralCode error: ${e.message}`);
}
saveResult({
email,
password: iterConfig.xiaomi.password,
refCode,
apiKey,
});
console.log('═'.repeat(70));
console.log(`✅ ITERATION ${idx + 1} DONE`);
console.log(` Email : ${email}`);
console.log(` RefCode : ${refCode || '(not found)'}`);
console.log(` ApiKey : ${apiKey || '(none)'}`);
console.log('═'.repeat(70));
return { ok: true, email, refCode, apiKey };
} catch (err) {
if (err.code === 'BALANCE_ZERO') {
console.log(`\n⚠ Regis Iteration ${idx + 1} skipped: ${err.message}`);
logFail(email, err.message);
return {
ok: false,
email,
error: err.message,
restricted: false,
stopReason: err.code,
};
}
console.error(`\n❌ Regis Iteration ${idx + 1} failed: ${err.message}`);
if (reg.page) {
try {
await reg.page.screenshot({
path: join(__dirname, `regis-error-${Date.now()}.png`),
fullPage: true,
});
} catch (e) {}
}
logFail(email, err.message);
return {
ok: false,
email,
error: err.message,
restricted: err.code === 'ACCOUNT_RESTRICTED' || err.code === 'BALANCE_NOT_CREDITED',
stopReason: err.code,
};
} finally {
if (reg.browser) {
await reg.browser.close().catch(() => {});
}
}
}
// ---- Main loop -------------------------------------------------------
async function main() {
console.log(`\nRegis loop config:`);
console.log(` Count : ${count}`);
console.log(` Output file : ${outputFile}`);
console.log(` Fail log : ${failLog}\n`);
let okCount = 0, failCount = 0;
for (let i = 0; i < count; i++) {
const result = await runIteration(i, count);
if (result.ok) {
okCount++;
} else {
failCount++;
if (result.restricted) {
const reason = result.stopReason || 'unknown';
console.error(`\n❌ Loop stopped: ${reason}`);
console.error(` Email: ${result.email}`);
console.error(` Detail: ${result.error}`);
break;
}
console.log(`\n! Iteration failed, retrying...`);
}
if (i < count - 1) {
const wait = 8000 + Math.floor(Math.random() * 6000);
console.log(` Waiting ${(wait / 1000).toFixed(1)}s before next...`);
await new Promise(r => setTimeout(r, wait));
}
}
console.log('\n' + '═'.repeat(70));
console.log(' REGIS LOOP SUMMARY');
console.log('═'.repeat(70));
console.log(` Total : ${count}`);
console.log(` Successful : ${okCount}`);
console.log(` Failed : ${failCount}`);
console.log(` Output : ${outputFile}`);
console.log('═'.repeat(70) + '\n');
process.exit(okCount > 0 ? 0 : 1);
}
main().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});