/** * 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; };