291 lines
10 KiB
JavaScript
291 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Chain loop. Lihat chain-loop-config.js untuk dokumentasi.
|
|
*
|
|
* Per iterasi:
|
|
* 1. Bikin tempmail
|
|
* 2. Override config.xiaomi.referralLink + .inviteCode dengan ref akun sebelumnya
|
|
* 3. Launch browser dengan random fingerprint
|
|
* 4. registerOnly() — sign up + verify email
|
|
* 5. redeemInviteCode() — pakai ref code dari akun sebelumnya (sudah otomatis
|
|
* lewat config override + flow di MimoRegistration)
|
|
* 6. getReferralCode() — ambil ref code akun ini buat iterasi berikutnya
|
|
* 7. createApiKey()
|
|
* 8. fillUltraspeedForm()
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
import { join } from 'path';
|
|
|
|
import { MimoRegistration, parseProxy } from './index.js';
|
|
import './extras.js'; // attach getReferralCode
|
|
import { generateFingerprint, buildInitScript, buildExtraHeaders } from './fingerprint.js';
|
|
|
|
import {
|
|
config, saveResult, logFail, count, seedRef, outputFile, failLog, __dirname, proxyList,
|
|
} from './chain-loop-config.js';
|
|
|
|
// ---- One-iteration runner --------------------------------------------
|
|
|
|
async function runIteration(idx, total, currentRef) {
|
|
console.log('\n' + '═'.repeat(70));
|
|
console.log(` [${idx + 1}/${total}] Chain Iteration (using ref: ${currentRef})`);
|
|
console.log('═'.repeat(70));
|
|
|
|
// Per-iter: clone config + override invite code & referral link
|
|
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 = currentRef;
|
|
iterConfig.xiaomi.referralLink =
|
|
`https://platform.xiaomimimo.com/?ref=${encodeURIComponent(currentRef)}`;
|
|
|
|
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
|
|
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 referral link (refCode chained)
|
|
console.log('[3/8] Opening referral 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. Redeem invite + create API key (idempotent — gpp meskipun udah)
|
|
// flow di MimoRegistration: clickConsoleMenu → balance → handleTerms →
|
|
// cek "Enter invite code" → kalau ada redeem, kalau tidak skip.
|
|
let redeemRestricted = false;
|
|
try {
|
|
await reg.redeemInviteCode();
|
|
} catch (e) {
|
|
if (e.code === 'ACCOUNT_RESTRICTED' || e.code === 'BALANCE_NOT_CREDITED') {
|
|
// Akun kena risk control / balance gak nambah — STOP propagate
|
|
// ke caller biar chain-loop berhenti.
|
|
const tag = e.code === 'ACCOUNT_RESTRICTED' ? 'restricted' : 'balance not credited';
|
|
console.log(` ❌ Stopping iteration — ${tag}: ${e.message}`);
|
|
redeemRestricted = true;
|
|
throw e;
|
|
}
|
|
console.log(` ! redeem error (lanjut): ${e.message}`);
|
|
}
|
|
|
|
// 8. Create API key (atau ambil existing)
|
|
console.log('[7/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 redeem/apikey/ultraspeed sebelumnya.
|
|
console.log('[8/8] Fetching referral code for next iteration...');
|
|
try {
|
|
const captured = await reg.getReferralCode();
|
|
if (captured && captured.toUpperCase() === currentRef.toUpperCase()) {
|
|
// Bug guard: kalau refCode == seed yg baru di-redeem, artinya
|
|
// getReferralCode tertipu (clipboard leak / modal salah / dst).
|
|
console.log(` ! refCode "${captured}" == seed "${currentRef}" — invalid, treating as null`);
|
|
try {
|
|
await reg.page.screenshot({
|
|
path: join(__dirname, `chain-refcode-leak-${Date.now()}.png`),
|
|
fullPage: true,
|
|
});
|
|
} catch (e) {}
|
|
refCode = null;
|
|
} else {
|
|
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,
|
|
invitedBy: currentRef,
|
|
});
|
|
|
|
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) {
|
|
console.error(`\n❌ Iteration ${idx + 1} failed: ${err.message}`);
|
|
if (reg.page) {
|
|
try {
|
|
await reg.page.screenshot({
|
|
path: join(__dirname, `chain-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(`\nChain loop config:`);
|
|
console.log(` Count : ${count}`);
|
|
console.log(` Seed ref : ${seedRef}`);
|
|
console.log(` Output file : ${outputFile}`);
|
|
console.log(` Fail log : ${failLog}\n`);
|
|
|
|
let currentRef = seedRef;
|
|
let okCount = 0, failCount = 0;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const result = await runIteration(i, count, currentRef);
|
|
|
|
if (result.ok) {
|
|
okCount++;
|
|
// Chain: kalau berhasil dapet refCode, pake itu untuk iterasi berikut
|
|
if (result.refCode) {
|
|
console.log(`\n→ Next iteration will use ref: ${result.refCode}`);
|
|
currentRef = result.refCode;
|
|
} else {
|
|
// refCode gak ke-capture — chain rusak. STOP biar gak bikin
|
|
// beberapa akun pakai invite code yang sama (bisa dianggap fraud
|
|
// sama Xiaomi).
|
|
console.error(`\n❌ Iteration ${i + 1} succeeded but didn't capture refCode.`);
|
|
console.error(` STOPPING chain to prevent reusing seed ${currentRef}.`);
|
|
console.error(` Run lagi dengan --seed ${currentRef} kalau mau lanjut manual.`);
|
|
break;
|
|
}
|
|
} else {
|
|
failCount++;
|
|
if (result.restricted) {
|
|
// Akun kena risk control / balance gak nambah — STOP loop, jangan retry
|
|
const reason = result.stopReason || 'unknown';
|
|
console.error(`\n❌ Chain stopped: ${reason}`);
|
|
console.error(` Email: ${result.email}`);
|
|
console.error(` Detail: ${result.error}`);
|
|
console.error(` Last seed used: ${currentRef}`);
|
|
break;
|
|
}
|
|
// Iter total fail (non-restriction) — currentRef gak di-update, retry pakai seed yang sama
|
|
console.log(`\n! Iteration failed, retrying with same seed ${currentRef}`);
|
|
}
|
|
|
|
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(' CHAIN 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);
|
|
});
|