Files
mimo-auto-reg/extras.js
T
2026-06-29 22:25:58 +07:00

300 lines
13 KiB
JavaScript

/**
* Method tambahan untuk MimoRegistration yang dipakai bersama oleh
* register-only.js dan chain-loop.js. Di-attach via prototype biar gak
* perlu modifikasi index.js.
*/
import { humanDelay } from './human.js';
import { MimoRegistration } from './index.js';
// Common English words yg sering ke-match regex code uppercase.
const REF_BLACKLIST = new Set([
'YOUR', 'CODE', 'INVITE', 'REFERRAL', 'ENTER', 'COPY', 'SHARE',
'EARN', 'NULL', 'NONE', 'TRUE', 'FALSE', 'EMPTY',
]);
/**
* Validate ref code — Xiaomi MiMo invite codes are always EXACTLY 6 chars
* alphanumeric uppercase. Sample valid: T9K59J, 4G7RWD, K3M2P8, HWPMXZ,
* AXQZ3F, TQB65X, P9LHUK — semua 6 char.
*
* Pattern lebih dari 6 char (mis. "DIMASSARI59" = 11 char username email)
* harus ditolak.
*/
function isValidRefCode(s) {
if (!s) return false;
const up = String(s).toUpperCase().trim();
if (up.length !== 6) return false;
if (REF_BLACKLIST.has(up)) return false;
return /^[A-Z0-9]{6}$/.test(up);
}
/**
* Ambil kode referal milik akun yang sedang login.
* Strategi:
* 1. Scan semua link/href yang ada `?ref=XXXXXX`
* 2. Cari teks "Invite code: XXXXXX" di body
* 3. Klik tombol "Refer & earn" / "Invite friends" → modal → scan + clipboard
*/
MimoRegistration.prototype.getReferralCode = async function () {
// Navigate ke balance page (kalau belum di sana)
const currentUrl = this.page.url();
if (!currentUrl.includes('/console/balance')) {
try { await this.clickConsoleMenu(); } catch (e) {}
if (!this.page.url().includes('/console/balance')) {
await this.page.goto('https://platform.xiaomimimo.com/console/balance', {
waitUntil: 'networkidle',
timeout: this.config.browser.timeout,
});
}
await this.handleOAuthRedirect();
await humanDelay(1500, 2500);
await this.handleTermsModal();
await this.waitForOverlaysGone();
}
// Strategi 1: scan ?ref= di link/anchor/data-clipboard
console.log(' Scanning for ?ref= links...');
let refCode = await this.page.evaluate(() => {
const anchors = Array.from(document.querySelectorAll('a, [data-href], [data-clipboard-text]'));
for (const a of anchors) {
const href = a.href || a.getAttribute('data-href') || a.getAttribute('data-clipboard-text') || '';
const m = href.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
const inputs = Array.from(document.querySelectorAll('input[readonly], textarea[readonly]'));
for (const inp of inputs) {
const v = inp.value || '';
const m = v.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
return null;
});
if (isValidRefCode(refCode)) return refCode;
if (refCode) console.log(` ! Strategi 1 hasil "${refCode}" ditolak (blacklist/invalid)`);
refCode = null;
// Strategi 2: regex teks plain — 3 pattern, hasil divalidasi
console.log(' Scanning page text for ref code patterns...');
refCode = await this.page.evaluate(() => {
const text = document.body.innerText;
// a) URL share di plain text: "...?ref=XXXXXX"
const m1 = text.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1) return m1[1].toUpperCase();
// b) "Invite code\nXXX" (modal Xiaomi nampilin label terus code di bawah,
// tanpa colon. Pakai [\s:\n]+ supaya match newline juga)
const m2 = text.match(/(?:invite\s+code|referral\s+code|your\s+code)[\s:\n]+([A-Z0-9]{6})\b/i);
if (m2) return m2[1].toUpperCase();
// c) "Code: XXX" generic dengan colon
const m3 = text.match(/\bcode\s*:\s*([A-Z0-9]{6})\b/i);
if (m3) return m3[1].toUpperCase();
return null;
});
if (isValidRefCode(refCode)) return refCode;
if (refCode) console.log(` ! Strategi 2 hasil "${refCode}" ditolak (blacklist/invalid)`);
refCode = null;
// Strategi 3: klik Refer & earn / Invite → modal → scan + clipboard
console.log(' Trying to click Refer & earn / Invite button...');
const opened = await this.page.evaluate(() => {
const all = Array.from(document.querySelectorAll('button, a, [role="button"]'));
const target = all.find(el => {
const txt = (el.textContent || '').trim();
return /^(Refer\s*&\s*earn|Refer\s*and\s*earn|Invite( friends)?|Share|Refer( friends)?)$/i.test(txt) && el.offsetHeight > 0;
});
if (target) {
target.scrollIntoView({ block: 'center' });
target.click();
return true;
}
return false;
});
if (opened) {
console.log(' ✓ Clicked Refer & earn / Invite button, waiting for modal...');
// Tunggu modal Refer & Earn beneran muncul (cari teks "Invite code" /
// "Invite builders" di salah satu modal yg visible)
await this.page.waitForFunction(() => {
const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'));
return modals.some(m => {
if (m.offsetHeight === 0 || m.style.display === 'none') return false;
const t = (m.innerText || '').toLowerCase();
return t.includes('invite code') || t.includes('invite builder') || t.includes('refer & earn');
});
}, { timeout: 8000 }).catch(() => {});
await humanDelay(800, 1400);
// Clear clipboard biar gak baca residue dari step sebelumnya
try {
await this.page.evaluate(async () => {
try { await navigator.clipboard.writeText(''); } catch (e) {}
});
} catch (e) {}
// Re-scan modal — coba beberapa kali kalau pertama belum ke-render
for (let attempt = 1; attempt <= 3 && !refCode; attempt++) {
const candidate = await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'))
.find(m => m.offsetHeight > 0 && (m.style.display !== 'none'));
const scope = modal || document.body;
// 1. Link share di anchor / data-clipboard / readonly input / value
const els = Array.from(scope.querySelectorAll('a, [data-clipboard-text], [data-href], input, textarea, span, div'));
for (const el of els) {
const sources = [
el.href, el.value, el.getAttribute('data-clipboard-text'),
el.getAttribute('data-href'), el.getAttribute('data-link'),
el.textContent,
];
for (const s of sources) {
if (!s) continue;
const m = s.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m) return m[1].toUpperCase();
}
}
// 2. Plain text di modal — coba 4 pattern (modal Xiaomi nampilin code
// sebagai standalone text dengan label "Invite code" di atasnya)
const text = scope.innerText || '';
// a) URL ?ref=XXX (paling reliable)
const m1 = text.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1) return m1[1].toUpperCase();
// b) "Invite code" label + code di baris berikut (format modal aktual)
const m2 = text.match(/(?:invite\s+code|referral\s+code|your\s+code)[\s:\n]+([A-Z0-9]{6})\b/i);
if (m2) return m2[1].toUpperCase();
// c) "Code: XXX" generic dengan colon
const m3 = text.match(/\bcode\s*:\s*([A-Z0-9]{6})\b/i);
if (m3) return m3[1].toUpperCase();
return null;
});
if (isValidRefCode(candidate)) {
refCode = candidate;
break;
}
if (candidate) {
console.log(` ! Modal attempt ${attempt} hasil "${candidate}" ditolak (blacklist)`);
}
if (attempt < 3) {
console.log(` Modal scan attempt ${attempt} empty, retry in 1.5s...`);
await humanDelay(1200, 1800);
}
}
if (!refCode) {
console.log(' ! Ref code not in modal text, trying Copy button + clipboard...');
try {
const ctx = this.page.context();
await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
// Strategi 3a: baca attribute langsung dari button Copy di modal
// REFER & EARN (bukan modal lain). Filter modal scope dulu
// dengan teks "Invite code" / "Invite builder".
const copyBtnText = await this.page.evaluate(() => {
const modals = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, [role="dialog"]'))
.filter(m => m.offsetHeight > 0);
// Cari modal yg teksnya mengandung "Invite code" / "Invite builder"
const referModal = modals.find(m => {
const t = (m.innerText || '').toLowerCase();
return t.includes('invite code') || t.includes('invite builder') || t.includes('refer & earn');
}) || modals[modals.length - 1]; // fallback ke modal terakhir
if (!referModal) return null;
const btns = Array.from(referModal.querySelectorAll('button, a, [role="button"], [data-clipboard-text]'));
const copy = btns.find(b => /^(copy|copy link|salin)/i.test((b.textContent || '').trim()));
if (!copy) return null;
return {
clipText: copy.getAttribute('data-clipboard-text'),
link: copy.getAttribute('data-link') || copy.getAttribute('data-href'),
value: copy.value,
};
});
if (copyBtnText) {
console.log(` Copy button attrs: ${JSON.stringify(copyBtnText)}`);
for (const v of Object.values(copyBtnText)) {
if (!v) continue;
const s = String(v);
// a) URL share dengan ?ref=XXX
const m = s.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m && isValidRefCode(m[1])) { refCode = m[1].toUpperCase(); break; }
// b) Bare code (TQB65X / P9LHUK)
const trimmed = s.trim().toUpperCase();
if (isValidRefCode(trimmed)) { refCode = trimmed; break; }
}
}
// Strategi 3b: kalau attribute kosong, klik Copy + bringToFront + readText
if (!refCode) {
await this.page.bringToFront().catch(() => {});
await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, .ant-modal-wrap, [role="dialog"]'))
.find(m => m.offsetHeight > 0);
if (!modal) return false;
const btns = Array.from(modal.querySelectorAll('button, a, [role="button"]'));
const copy = btns.find(b => /^(copy|copy link|salin)/i.test((b.textContent || '').trim()));
if (copy) { copy.click(); return true; }
return false;
});
await humanDelay(900, 1400);
const clipboardText = await this.page.evaluate(async () => {
// Pastikan window punya focus dulu
try { window.focus(); } catch (e) {}
try {
return await navigator.clipboard.readText();
} catch (e) {
return '';
}
});
if (clipboardText) {
console.log(` Clipboard: ${clipboardText.substring(0, 100)}`);
// Coba ekstrak ref code dari clipboard:
// a) URL share dengan ?ref=XXX
// b) Bare code (TQB65X / P9LHUK / dst) — modal Xiaomi cuma copy code
const m1 = clipboardText.match(/[?&]ref=([A-Z0-9]{6})\b/i);
if (m1 && isValidRefCode(m1[1])) {
refCode = m1[1].toUpperCase();
} else {
// Bare code: trim, uppercase, validasi
const trimmed = clipboardText.trim().toUpperCase();
if (isValidRefCode(trimmed)) {
refCode = trimmed;
} else {
// Cari token alphanumeric 4-12 char di mana pun di clipboard
const m2 = clipboardText.match(/\b([A-Z0-9]{6})\b/i);
if (m2 && isValidRefCode(m2[1])) refCode = m2[1].toUpperCase();
}
}
} else {
console.log(' ! Clipboard empty (page may have lost focus)');
}
}
} catch (clipErr) {
console.log(` ! Clipboard read error: ${clipErr.message}`);
}
}
// Kalau masih null, dump screenshot + html modal untuk debug
if (!refCode) {
console.log(' ! All strategies failed — dumping debug artifacts');
try {
const ts = Date.now();
await this.page.screenshot({
path: `error-refcode-${ts}.png`,
fullPage: false,
});
const modalHtml = await this.page.evaluate(() => {
const modal = Array.from(document.querySelectorAll('.ant-modal, .ant-modal-content, [role="dialog"]'))
.find(m => m.offsetHeight > 0);
return modal ? modal.outerHTML.substring(0, 2000) : 'NO_MODAL';
});
console.log(` Modal HTML (preview): ${modalHtml.substring(0, 500)}`);
} catch (e) {}
}
await this.page.keyboard.press('Escape').catch(() => {});
}
return refCode;
};