anprar/dicoding-api-bulk-pro
0
1// src/automation.js - Browser automation helper for Dicoding registration with Puppeteer Extra2// This script runs on a separate server (Render/Railway)3 4const puppeteer = require('puppeteer-extra')5const StealthPlugin = require('puppeteer-extra-plugin-stealth')6 7// Apply stealth plugin8puppeteer.use(StealthPlugin())9 10// Helper: send log message to Telegram chat11async function tgLog(botToken, chatId, message) {12 if (!botToken || !chatId) return13 try {14 await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {15 method: 'POST',16 headers: { 'Content-Type': 'application/json' },17 body: JSON.stringify({18 chat_id: chatId,19 text: message,20 parse_mode: 'HTML'21 })22 })23 } catch (e) {24 // silently fail - don't break automation because of a log message25 }26}27 28class DicodingAutomator {29 constructor(botToken, chatId, externalBrowser = null) {30 this.browser = externalBrowser31 this.isSharedBrowser = !!externalBrowser32 this.page = null33 this.botToken = botToken34 this.chatId = chatId35 this.isCancelled = false36 }37 38 async log(msg, forceTelegram = false) {39 // Selalu log ke console server untuk debugging40 console.log(msg)41 42 // Filter pesan mana yang boleh dikirim ke Telegram (biar bersih)43 const isError = msg.includes('โ') || msg.includes('๐') || msg.includes('Error');44 const isSuccess = msg.includes('Pendaftaran Phase 2 TUNTAS') || msg.includes('Form berhasil dikirim');45 const isMilestone = msg.includes('Memulai pendaftaran') || msg.includes('Mengulangi Phase 2') || msg.includes('Sedang memproses, mohon tunggu') || forceTelegram;46 47 // Hide spammy logs (mengisi form, klik tombol, cek validasi, dll) di Telegram48 if (isError || isSuccess || isMilestone) {49 await tgLog(this.botToken, this.chatId, `๐ค <i>${msg}</i>`)50 }51 }52 53 // Check if cancelled and throw early-exit if so54 checkCancelled() {55 if (this.isCancelled) {56 throw new Error('CANCELLED')57 }58 }59 60 // Force-kill the browser session (called from /cancel endpoint)61 async cancel() {62 this.isCancelled = true63 await this.log('๐ Perintah pembatalan diterima! Menghentikan proses...')64 await this.close()65 }66 67 async init() {68 if (!this.browser) {69 this.browser = await puppeteer.launch({70 headless: 'new',71 executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || null,72 args: [73 '--no-sandbox',74 '--disable-setuid-sandbox',75 '--disable-dev-shm-usage', // Penting untuk container Docker/Render76 '--disable-accelerated-2d-canvas',77 '--no-first-run',78 '--no-zygote',79 '--disable-gpu',80 '--window-size=1280,800',81 '--incognito',82 '--disable-background-networking',83 '--disable-extensions',84 '--mute-audio'85 ]86 })87 this.isSharedBrowser = false88 }89 this.context = await this.browser.createIncognitoBrowserContext()90 this.page = await this.context.newPage()91 await this.page.setViewport({ width: 1280, height: 800 })92 await this.page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')93 94 // Optimasi Memori Ekstrem: Blokir semua asset visual yang tidak berguna untuk bot95 await this.page.setRequestInterception(true);96 this.page.on('request', (req) => {97 const resourceType = req.resourceType();98 if (resourceType === 'image' || resourceType === 'stylesheet' || resourceType === 'font' || resourceType === 'media') {99 req.abort();100 } else {101 req.continue();102 }103 });104 }105 106 // Phase 1: Main Dicoding Account Registration107 async registerPhase1(userData) {108 try {109 this.checkCancelled()110 await this.log('โณ Sedang memproses pendaftaran akun Phase 1 di latar belakang, mohon tunggu...', true)111 await this.init()112 113 this.checkCancelled()114 await this.log('๐ Membuka halaman AWS Dicoding (referral link)...')115 await this.page.goto('https://aws.dicoding.com/register?ref=39249', {116 waitUntil: 'domcontentloaded',117 timeout: 60000118 })119 await this.log(`๐ URL setelah redirect: ${this.page.url()}`)120 121 const pageTitle = await this.page.title()122 await this.log(`โ
Halaman dimuat: "${pageTitle}"`)123 124 // If redirected to login page, find and click the "Daftar akun Dicoding" link125 const currentUrl = this.page.url()126 if (currentUrl.includes('/login') || currentUrl.includes('/signin')) {127 await this.log('๐ Terdeteksi halaman Login. Mencari link Daftar akun Dicoding...')128 129 // Exact selector for the registration button on the AWS login page130 const daftarLink = await this.page.$('a[href*="register/dicoding"]')131 132 if (daftarLink) {133 await this.log('๐ฑ๏ธ Klik link Daftar...')134 await Promise.all([135 this.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),136 daftarLink.click()137 ])138 await this.log(`๐ URL setelah klik Daftar: ${this.page.url()}`)139 } else {140 await this.log('โ ๏ธ Link Daftar tidak ditemukan. Langsung navigasi ke registrasi...')141 await this.page.goto('https://www.dicoding.com/registration', {142 waitUntil: 'domcontentloaded',143 timeout: 60000144 })145 await this.log(`๐ URL: ${this.page.url()}`)146 }147 }148 149 // Step 1: Fill name - use #name (specific ID)150 this.checkCancelled()151 await this.log('๐ Step 1/4: Mengisi nama...')152 await this.page.waitForSelector('#name, input[name="name"]', { timeout: 60000 })153 const nameInput = await this.page.$('#name') || await this.page.$('input[name="name"]')154 await nameInput.click({ clickCount: 3 })155 await nameInput.type(userData.name, { delay: 50 })156 await this.log(`โ
Nama diisi: "${userData.name}"`)157 158 // Step 2: Fill email - use #email (specific ID)159 this.checkCancelled()160 await this.log('๐ Step 2/4: Mengisi email...')161 await this.page.waitForSelector('#email, input[name="email"]', { timeout: 30000 })162 const emailInput = await this.page.$('#email') || await this.page.$('input[name="email"]')163 await emailInput.click({ clickCount: 3 })164 await emailInput.type(userData.email, { delay: 50 })165 await this.log(`โ
Email diisi: "${userData.email}"`)166 167 // Step 3: Fill password - use #password ONLY (not all password inputs, since login form is also on the page)168 this.checkCancelled()169 await this.log('๐ Step 3/4: Mengisi password...')170 await this.page.waitForSelector('#password, input[name="password"]', { timeout: 30000 })171 const passInput = await this.page.$('#password') || await this.page.$('input[name="password"]')172 await passInput.click({ clickCount: 3 })173 await passInput.type(userData.password, { delay: 50 })174 await this.log('โ
Password diisi')175 176 // Step 4: Click "Daftar" button - use button.gtm-reg (unique class for register button)177 // DO NOT use button[type="submit"] as it picks up the "Masuk" (login) button!178 this.checkCancelled()179 await this.log('๐ Step 4/4: Menekan tombol Daftar...')180 await this.page.waitForSelector('button.gtm-reg, button[type="submit"]', { timeout: 15000 })181 await this.log('๐ฑ๏ธ Klik tombol "Daftar"...')182 183 await Promise.all([184 this.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {185 console.log('[Phase 1] Navigation timeout after submit (AJAX form)')186 }),187 this.page.click('button.gtm-reg')188 ])189 this.checkCancelled()190 // Wait silently for the server to process the registration191 await this.page.waitForTimeout(5000)192 193 const url = this.page.url()194 await this.log('โ
Form berhasil dikirim!\n\nSilakan cek kotak masuk dan klik link verifikasi email dari Dicoding.\nJika sudah, ketik atau klik ๐ /verify untuk melanjutkan ke penambahan Phase 2.')195 196 return {197 success: true,198 message: 'Account created or pending verification.',199 url: url200 }201 202 } catch (error) {203 if (error.message === 'CANCELLED' || this.isCancelled) {204 await this.log('๐ Proses dihentikan oleh pengguna.')205 return { success: false, error: 'Dibatalkan oleh pengguna', cancelled: true }206 }207 if (error.message.includes('Protocol error') || error.message.includes('Target closed') || error.message.includes('Session closed')) {208 await tgLog(this.botToken, this.chatId, '๐ <i>Proses dihentikan oleh pengguna.</i>')209 return { success: false, error: 'Dibatalkan oleh pengguna', cancelled: true }210 }211 await this.log(`โ ERROR: ${error.message}`)212 return { success: false, error: error.message }213 } finally {214 if (!this.keepAlive) {215 await this.close()216 }217 }218 }219 220 // Phase 2: Login and Fill AWS Specific Data (Full Incognito - Clean Session)221 async registerPhase2(userData, preVerifyUrl = null) {222 try {223 this.checkCancelled()224 225 await this.log('โณ Sedang memproses pendaftaran form AWS di latar belakang, mohon tunggu...', true)226 await this.init()227 228 // Holy Grail Auto-Pilot Mode: Verify email first in the same session229 if (preVerifyUrl) {230 await this.log('๐ Membuka tautan verifikasi rahasia dari kotak masuk...')231 await this.page.goto(preVerifyUrl, { waitUntil: 'domcontentloaded', timeout: 60000 })232 await this.page.waitForTimeout(4000)233 await this.log('โ
Akun dipastikan aktif! Menuju penetrasi formulir AWS.')234 }235 236 // ===== STRATEGY: Login ke Dicoding DULU, baru buka AWS referral =====237 // Login di halaman OAuth (login?client_id=aws) gagal di headless browser.238 // Solusi: login ke www.dicoding.com/login langsung (tanpa OAuth params),239 // lalu navigasi ke aws.dicoding.com. Session sudah aktif jadi OAuth auto-redirect.240 241 // ===== STEP 1: Login langsung ke Dicoding =====242 this.checkCancelled()243 await this.log('๐ Login ke Dicoding...')244 await this.page.goto('https://www.dicoding.com/login', {245 waitUntil: 'domcontentloaded',246 timeout: 60000247 })248 await this.page.waitForTimeout(3000)249 250 // Isi email & password251 this.checkCancelled()252 await this.log('โ๏ธ Mengisi form login...')253 const emailField = await this.page.waitForSelector('input[type="email"], input[name="email"], input[name="login_email"], #email', { timeout: 30000 }).catch(() => null)254 if (!emailField) {255 throw new Error('Form login tidak ditemukan!')256 }257 258 await emailField.click({ clickCount: 3 })259 await emailField.type(userData.email, { delay: 40 })260 261 const passField = await this.page.$('input[type="password"], input[name="password"], input[name="login_password"], #password')262 if (passField) {263 await passField.click({ clickCount: 3 })264 await passField.type(userData.password, { delay: 40 })265 }266 await this.log('โ
Email & password diisi.')267 268 // Submit via Enter pada password field269 this.checkCancelled()270 if (passField) {271 await passField.focus()272 }273 await this.page.keyboard.press('Enter')274 275 // Tunggu login selesai - cek setiap 2 detik selama max 20 detik276 let loginSuccess = false277 for (let i = 0; i < 10; i++) {278 await this.page.waitForTimeout(2000)279 const url = this.page.url()280 if (!url.includes('/login') || url.includes('session-duplication')) {281 loginSuccess = true282 break283 }284 }285 286 if (!loginSuccess) {287 // Fallback: coba klik tombol Masuk288 await this.log('โ ๏ธ Login via Enter gagal, mencoba klik tombol...')289 const submitBtn = await this.page.$('button[type="submit"]')290 if (submitBtn) {291 await submitBtn.click().catch(() => {})292 }293 // Tunggu lagi294 for (let i = 0; i < 5; i++) {295 await this.page.waitForTimeout(2000)296 const url = this.page.url()297 if (!url.includes('/login') || url.includes('session-duplication')) {298 loginSuccess = true299 break300 }301 }302 }303 304 // Cek apakah ada error login (email/password salah)305 if (!loginSuccess) {306 const loginPageText = await this.page.evaluate(() => document.body ? document.body.innerText : '').catch(() => '')307 if (loginPageText.toLowerCase().includes('salah') || loginPageText.toLowerCase().includes('invalid') || loginPageText.toLowerCase().includes('incorrect')) {308 throw new Error('Email atau password salah!')309 }310 await this.log(`โ ๏ธ Tetap di halaman login. URL: ${this.page.url()}`)311 await this.log(`๐ Page text: ${loginPageText.substring(0, 300)}`)312 }313 314 await this.log(`โ
Login selesai. URL: ${this.page.url()}`)315 316 // ===== STEP 2: Handle session duplication =====317 this.checkCancelled()318 const currentUrl = this.page.url()319 const pageContent = await this.page.evaluate(() => document.body ? document.body.innerText.toLowerCase() : '')320 321 if (currentUrl.includes('session-duplication') || pageContent.includes('akun terdeteksi') || pageContent.includes('browser lain')) {322 await this.log('โ ๏ธ Halaman duplikasi sesi terdeteksi! Menekan "Lanjut di browser ini"...')323 324 const dupBtn = await this.page.$('#login-here')325 if (dupBtn) {326 await dupBtn.evaluate(el => el.click()).catch(() => {})327 } else {328 const btnHandle = await this.page.evaluateHandle(() => {329 const buttons = Array.from(document.querySelectorAll('button, a'))330 return buttons.find(b => 331 b.innerText.toLowerCase().includes('lanjut di browser ini') ||332 b.innerText.toLowerCase().includes('gunakan halaman ini')333 ) || null334 })335 if (btnHandle && btnHandle.asElement()) {336 await btnHandle.asElement().evaluate(el => el.click()).catch(() => {})337 }338 }339 await this.page.waitForTimeout(8000)340 await this.log(`โ
Sesi browser dikonfirmasi. URL: ${this.page.url()}`)341 }342 343 // ===== STEP 3: Buka halaman referral AWS (session sudah aktif) =====344 this.checkCancelled()345 await this.log('๐ Membuka halaman referral AWS Dicoding...')346 await this.page.goto('https://aws.dicoding.com/register?ref=39249', {347 waitUntil: 'domcontentloaded',348 timeout: 60000349 })350 await this.page.waitForTimeout(5000)351 await this.log(`๐ URL sekarang: ${this.page.url()}`)352 353 // Jika ada tombol "Masuk akun Dicoding", klik - karena session sudah aktif,354 // OAuth harusnya auto-redirect ke form tanpa minta login lagi355 const masukBtnHandle = await this.page.evaluateHandle(() => {356 const links = Array.from(document.querySelectorAll('a, button'))357 return links.find(el => el.innerText.toLowerCase().includes('masuk akun dicoding')) || null358 })359 if (masukBtnHandle && masukBtnHandle.asElement()) {360 await this.log('๐ Klik "Masuk akun Dicoding" (sesi sudah aktif)...')361 await masukBtnHandle.asElement().evaluate(el => el.click()).catch(() => {})362 await this.page.waitForTimeout(10000)363 await this.log(`๐ URL setelah masuk: ${this.page.url()}`)364 }365 366 // Handle OAuth authorize page jika muncul367 if (this.page.url().includes('oauth') || this.page.url().includes('authorize')) {368 await this.log('๐ Halaman otorisasi terdeteksi...')369 const authBtn = await this.page.evaluateHandle(() => {370 const buttons = Array.from(document.querySelectorAll('button'))371 return buttons.find(b => 372 b.innerText.toLowerCase().includes('izinkan') || 373 b.innerText.toLowerCase().includes('setuju')374 ) || document.querySelector('button[type="submit"]') || null375 })376 if (authBtn && authBtn.asElement()) {377 await authBtn.asElement().evaluate(el => el.click()).catch(() => {})378 await this.page.waitForTimeout(8000)379 }380 }381 382 // Handle session duplication lagi jika muncul di AWS383 if (this.page.url().includes('session-duplication')) {384 await this.log('โ ๏ธ Session duplication muncul lagi di AWS...')385 const dupBtn2 = await this.page.$('#login-here')386 if (dupBtn2) {387 await dupBtn2.evaluate(el => el.click()).catch(() => {})388 } else {389 await this.page.evaluate(() => {390 const btns = Array.from(document.querySelectorAll('button, a'))391 const btn = btns.find(b => b.innerText.toLowerCase().includes('lanjut di browser ini'))392 if (btn) btn.click()393 }).catch(() => {})394 }395 await this.page.waitForTimeout(8000)396 397 // Navigasi ulang ke referral398 await this.page.goto('https://aws.dicoding.com/register?ref=39249', {399 waitUntil: 'domcontentloaded', timeout: 60000400 })401 await this.page.waitForTimeout(5000)402 403 // Klik masuk lagi404 await this.page.evaluate(() => {405 const links = Array.from(document.querySelectorAll('a, button'))406 const btn = links.find(el => el.innerText.toLowerCase().includes('masuk akun dicoding'))407 if (btn) btn.click()408 }).catch(() => {})409 await this.page.waitForTimeout(10000)410 }411 412 // Jika masih di halaman login (belum masuk form), coba login ulang di OAuth413 if (this.page.url().includes('www.dicoding.com/login')) {414 await this.log('โ ๏ธ Masih di halaman login OAuth, mencoba isi ulang...')415 const emailField2 = await this.page.$('input[type="email"], input[name="email"], #email')416 if (emailField2) {417 await emailField2.click({ clickCount: 3 })418 await emailField2.type(userData.email, { delay: 30 })419 const passField2 = await this.page.$('input[type="password"], input[name="password"], #password')420 if (passField2) {421 await passField2.click({ clickCount: 3 })422 await passField2.type(userData.password, { delay: 30 })423 await passField2.focus()424 await this.page.keyboard.press('Enter')425 await this.page.waitForTimeout(10000)426 }427 }428 }429 430 await this.log(`๐ Halaman form: ${this.page.url()}`)431 432 // Verifikasi bahwa kita sudah di halaman form433 if (this.page.url().includes('login') && !this.page.url().includes('aws.dicoding.com/registration')) {434 throw new Error(`Gagal masuk ke halaman form. Masih di: ${this.page.url()}. Session mungkin tidak valid.`)435 }436 437 this.checkCancelled()438 439 // ===== STEP 5: Isi form "Isi Data Diri" =====440 await this.log('๐ Mulai mengisi form data diri AWS...')441 await this.page.waitForTimeout(3000)442 443 // 5a. Nomor handphone444 this.checkCancelled()445 await this.log('๐ Mengisi nomor handphone...')446 const phoneFilled = await this.page.evaluate((phone) => {447 // Cari semua input fields448 const inputs = Array.from(document.querySelectorAll('input'))449 // Cari yang placeholder-nya mengandung "handphone" atau "nomor"450 let phoneInput = inputs.find(i => 451 (i.placeholder && (i.placeholder.toLowerCase().includes('handphone') || i.placeholder.toLowerCase().includes('nomor'))) ||452 (i.name && (i.name.toLowerCase().includes('phone') || i.name.toLowerCase().includes('handphone')))453 )454 // Fallback: cari input di dekat label "Nomor handphone"455 if (!phoneInput) {456 const labels = Array.from(document.querySelectorAll('label'))457 const phoneLabel = labels.find(l => l.innerText.toLowerCase().includes('nomor handphone'))458 if (phoneLabel) {459 const container = phoneLabel.closest('div') || phoneLabel.parentElement460 if (container) {461 phoneInput = container.querySelector('input[type="text"], input[type="tel"], input:not([type="checkbox"]):not([type="radio"])')462 }463 }464 }465 // Fallback: cari input di dekat "+62" prefix466 if (!phoneInput) {467 const allElements = Array.from(document.querySelectorAll('*'))468 const prefix62 = allElements.find(el => el.innerText.trim() === '+62')469 if (prefix62) {470 const container = prefix62.closest('div')471 if (container) {472 phoneInput = container.querySelector('input')473 }474 }475 }476 if (phoneInput) {477 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set478 nativeInputValueSetter.call(phoneInput, phone)479 phoneInput.dispatchEvent(new Event('input', { bubbles: true }))480 phoneInput.dispatchEvent(new Event('change', { bubbles: true }))481 return true482 }483 return false484 }, userData.phone)485 await this.log(phoneFilled ? `โ
Nomor HP diisi: ${userData.phone}` : 'โ ๏ธ Field nomor HP tidak ditemukan')486 487 // 5b. Tanggal lahir (via JS injection untuk hindari date picker)488 this.checkCancelled()489 await this.log('๐
Mengisi tanggal lahir...')490 const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')491 const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')492 const year = String(Math.floor(Math.random() * 16) + 1985)493 const birthday = `${year}-${month}-${day}` // flatpickr uses yyyy-mm-dd494 495 const birthdayFilled = await this.page.evaluate((bday) => {496 const inputs = Array.from(document.querySelectorAll('input'))497 let birthInput = inputs.find(i => 498 (i.placeholder && (i.placeholder.toLowerCase().includes('lahir') || i.placeholder.toLowerCase().includes('tanggal'))) ||499 (i.name && (i.name.toLowerCase().includes('birth') || i.name.toLowerCase().includes('lahir')))500 )501 // Fallback: cari via label502 if (!birthInput) {503 const labels = Array.from(document.querySelectorAll('label'))504 const birthLabel = labels.find(l => l.innerText.toLowerCase().includes('tanggal lahir'))505 if (birthLabel) {506 const container = birthLabel.closest('div') || birthLabel.parentElement507 if (container) {508 birthInput = container.querySelector('input')509 }510 }511 }512 if (birthInput) {513 // Set value via flatpickr instance if exists514 if (birthInput._flatpickr) {515 birthInput._flatpickr.setDate(bday, true)516 return true517 }518 // Fallback ke native setter519 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set520 nativeInputValueSetter.call(birthInput, bday)521 birthInput.dispatchEvent(new Event('input', { bubbles: true }))522 birthInput.dispatchEvent(new Event('change', { bubbles: true }))523 return true524 }525 return false526 }, birthday)527 await this.log(birthdayFilled ? `โ
Tanggal lahir diisi: ${birthday}` : 'โ ๏ธ Field tanggal lahir tidak ditemukan')528 await this.page.waitForTimeout(500)529 530 // 5c. Jenis kelamin: Laki-laki531 this.checkCancelled()532 await this.log('๐ค Memilih jenis kelamin: Laki-laki...')533 const genderSelected = await this.page.evaluate(() => {534 const labels = Array.from(document.querySelectorAll('label'))535 const label = labels.find(l => l.innerText.trim() === 'Laki-laki')536 if (label) { label.click(); return true }537 const radios = document.querySelectorAll('input[type="radio"]')538 for (const r of radios) {539 if (r.value.toLowerCase().includes('laki') || r.value.toLowerCase() === 'male' || r.value === 'L') {540 r.click(); return true541 }542 }543 return false544 })545 await this.log(genderSelected ? 'โ
Jenis kelamin dipilih' : 'โ ๏ธ Jenis kelamin tidak ditemukan')546 547 // 5d. LinkedIn: Tidak548 this.checkCancelled()549 await this.log('๐ LinkedIn: Tidak...')550 const linkedinSelected = await this.page.evaluate(() => {551 const allText = document.body.innerText552 // Cari semua group fields yang mengandung kata linkedin553 const allDivs = Array.from(document.querySelectorAll('div, fieldset, section'))554 for (const div of allDivs) {555 const divText = div.innerText.toLowerCase()556 if (divText.includes('linkedin') && div.querySelectorAll('label').length > 0 && div.querySelectorAll('label').length <= 5) {557 const labels = div.querySelectorAll('label')558 for (const l of labels) {559 if (l.innerText.trim() === 'Tidak') { l.click(); return true }560 }561 }562 }563 return false564 })565 await this.log(linkedinSelected ? 'โ
LinkedIn dipilih' : 'โ ๏ธ LinkedIn tidak ditemukan')566 567 // 5e. Profil: Pencari kerja (dropdown)568 this.checkCancelled()569 await this.log('๐ Profil: Pencari kerja...')570 const profileSelected = await this.page.evaluate(() => {571 const selects = document.querySelectorAll('select')572 for (const sel of selects) {573 for (const opt of sel.options) {574 if (opt.text.toLowerCase().includes('pencari kerja')) {575 sel.value = opt.value576 sel.dispatchEvent(new Event('change', { bubbles: true }))577 return true578 }579 }580 }581 return false582 })583 await this.log(profileSelected ? 'โ
Profil dipilih: Pencari kerja' : 'โ ๏ธ Dropdown profil tidak ditemukan')584 585 // 5f. Sedang menempuh pendidikan: Tidak586 this.checkCancelled()587 await this.log('๐ Sedang menempuh pendidikan: Tidak...')588 const eduSelected = await this.page.evaluate(() => {589 const allDivs = Array.from(document.querySelectorAll('div, fieldset, section'))590 for (const div of allDivs) {591 const text = div.innerText.toLowerCase()592 if (text.includes('sedang menempuh pendidikan') && div.querySelectorAll('label').length <= 5) {593 const labels = div.querySelectorAll('label')594 for (const l of labels) {595 if (l.innerText.trim() === 'Tidak') { l.click(); return true }596 }597 }598 }599 return false600 })601 await this.log(eduSelected ? 'โ
Pendidikan dipilih' : 'โ ๏ธ Pendidikan tidak ditemukan')602 603 // 5g. Sedang bekerja: Tidak604 this.checkCancelled()605 await this.log('๐ผ Sedang bekerja: Tidak...')606 const workSelected = await this.page.evaluate(() => {607 const allDivs = Array.from(document.querySelectorAll('div, fieldset, section'))608 for (const div of allDivs) {609 const text = div.innerText.toLowerCase()610 if (text.includes('sedang bekerja') && !text.includes('menempuh') && div.querySelectorAll('label').length <= 5) {611 const labels = div.querySelectorAll('label')612 for (const l of labels) {613 if (l.innerText.trim() === 'Tidak') { l.click(); return true }614 }615 }616 }617 return false618 })619 await this.log(workSelected ? 'โ
Status bekerja dipilih' : 'โ ๏ธ Sedang bekerja tidak ditemukan')620 621 // 5h. Pendidikan terakhir: Diploma IV/Strata I622 this.checkCancelled()623 await this.log('๐ Pendidikan terakhir: DiplomaIV/Strata I...')624 const lastEduSelected = await this.page.evaluate(() => {625 // Coba cari jika ini adalah dropdown (select)626 const selects = document.querySelectorAll('select')627 for (const sel of selects) {628 const parentText = sel.parentElement?.innerText?.toLowerCase() || ''629 const name = sel.name?.toLowerCase() || ''630 if (name.includes('education') || name.includes('pendidikan') || parentText.includes('pendidikan terakhir')) {631 for (let i = 0; i < sel.options.length; i++) {632 const optText = sel.options[i].text.toLowerCase()633 if (optText.includes('diploma iv') || optText.includes('diplomaiv') || 634 optText.includes('strata 1') || optText.includes('strata i') || optText.includes('s1')) {635 sel.value = sel.options[i].value636 sel.dispatchEvent(new Event('change', { bubbles: true }))637 return true638 }639 }640 }641 }642 643 // Coba cari dalam group Pendidikan Terakhir644 const allGroups = Array.from(document.querySelectorAll('div, form, fieldset'))645 for (const grp of allGroups) {646 const titleEl = grp.querySelector('label, h3, h4, p, span')647 if (titleEl && titleEl.innerText && titleEl.innerText.toLowerCase().includes('pendidikan terakhir')) {648 const radios = grp.querySelectorAll('input[type="radio"]')649 if (radios.length > 0) {650 // Cari radio yang labelnya mengandung kata Diploma/Strata651 for (const r of radios) {652 // Label mungkin parent atau punya attribute for, atau teks setelahnya653 const rParentText = r.parentElement ? r.parentElement.innerText.toLowerCase() : ''654 const idAttr = r.id;655 let rLabelText = '';656 if (idAttr) {657 const lbl = document.querySelector(`label[for="${idAttr}"]`)658 if (lbl) rLabelText = lbl.innerText.toLowerCase()659 }660 const combinedText = rParentText + ' ' + rLabelText661 662 if (combinedText.includes('diploma iv') || combinedText.includes('diplomaiv') || combinedText.includes('strata i') || combinedText.includes('s1')) {663 r.checked = true664 r.dispatchEvent(new Event('input', { bubbles: true }))665 r.dispatchEvent(new Event('change', { bubbles: true }))666 r.click()667 return true668 }669 }670 // Jika tidak ketemu berdasarkan teks, tapi ada radio buttons, asumsi urutan ke-7 adalah DiplomaIV671 if (radios.length >= 7) {672 const r = radios[radios.length > 7 ? 6 : radios.length - 1] // indeks ke-6 adalah item ke-7673 r.checked = true674 r.dispatchEvent(new Event('input', { bubbles: true }))675 r.dispatchEvent(new Event('change', { bubbles: true }))676 r.click()677 return true678 }679 }680 }681 }682 683 // Coba metode global jika tidak ada group khusus684 const radios = Array.from(document.querySelectorAll('input[type="radio"]'))685 for (const r of radios) {686 const textCtx = (r.parentElement ? r.parentElement.innerText : '') + (r.id && document.querySelector(`label[for="${r.id}"]`) ? document.querySelector(`label[for="${r.id}"]`).innerText : '')687 if (textCtx.toLowerCase().includes('diplomaiv') || textCtx.toLowerCase().includes('strata i')) {688 r.checked = true689 r.dispatchEvent(new Event('input', { bubbles: true }))690 r.dispatchEvent(new Event('change', { bubbles: true }))691 r.click()692 return true693 }694 }695 return false696 })697 await this.log(lastEduSelected ? 'โ
Pendidikan terakhir dipilih' : 'โ ๏ธ Pendidikan terakhir tidak ditemukan')698 699 // 5i. Pemahaman dasar IT: Ya700 this.checkCancelled()701 await this.log('๐ป Pemahaman dasar IT: Ya...')702 const itSelected = await this.page.evaluate(() => {703 const allDivs = Array.from(document.querySelectorAll('div, fieldset, section'))704 for (const div of allDivs) {705 const text = div.innerText.toLowerCase()706 if ((text.includes('pemahaman dasar') || text.includes('latar belakang')) && text.includes('it') && div.querySelectorAll('label').length <= 5) {707 const labels = div.querySelectorAll('label')708 for (const l of labels) {709 if (l.innerText.trim() === 'Ya') { l.click(); return true }710 }711 }712 }713 return false714 })715 await this.log(itSelected ? 'โ
IT background dipilih' : 'โ ๏ธ IT background tidak ditemukan')716 717 // 5j. Domisili (dropdown/Select2)718 this.checkCancelled()719 await this.log('๐๏ธ Memilih domisili...')720 const domicileSelected = await this.page.evaluate(() => {721 const selects = document.querySelectorAll('select')722 for (const sel of selects) {723 const parent = sel.closest('div')724 const label = parent?.querySelector('label')?.innerText || sel.name || ''725 if (label.toLowerCase().includes('domisili') || sel.name.toLowerCase().includes('domisili') || 726 sel.name.toLowerCase().includes('city') || sel.name.toLowerCase().includes('kota')) {727 if (sel.options.length > 1) {728 sel.value = sel.options[1].value729 sel.dispatchEvent(new Event('change', { bubbles: true }))730 return 'select'731 }732 }733 }734 // Fallback: Select2735 const select2 = document.querySelector('.select2-container, span.select2')736 if (select2) { select2.click(); return 'select2' }737 return false738 })739 740 if (domicileSelected === 'select2') {741 await this.page.waitForTimeout(500)742 const searchInput = await this.page.$('.select2-search__field, .select2-search input')743 if (searchInput) {744 await searchInput.type('Jakarta', { delay: 50 })745 await this.page.waitForTimeout(1500)746 const firstResult = await this.page.$('.select2-results__option, .select2-results li')747 if (firstResult) {748 await firstResult.click()749 await this.log('โ
Domisili dipilih (Select2)')750 }751 }752 } else if (domicileSelected === 'select') {753 await this.log('โ
Domisili dipilih (dropdown)')754 } else {755 await this.log('โ ๏ธ Domisili tidak ditemukan')756 }757 758 // 5k. Disabilitas: Tidak759 this.checkCancelled()760 await this.log('โฟ Disabilitas: Tidak...')761 const disabilitySelected = await this.page.evaluate(() => {762 const allDivs = Array.from(document.querySelectorAll('div, fieldset, section'))763 for (const div of allDivs) {764 if (div.innerText.toLowerCase().includes('disabilitas') && div.querySelectorAll('label').length <= 5) {765 const labels = div.querySelectorAll('label')766 for (const l of labels) {767 if (l.innerText.trim() === 'Tidak') { l.click(); return true }768 }769 }770 }771 return false772 })773 await this.log(disabilitySelected ? 'โ
Disabilitas dipilih' : 'โ ๏ธ Disabilitas tidak ditemukan')774 775 // 5l. Sumber info: Teman/Relasi/Kerabat776 this.checkCancelled()777 await this.log('๐ฃ Sumber info: Teman/Relasi/Kerabat...')778 const sourceSelected = await this.page.evaluate(() => {779 const labels = Array.from(document.querySelectorAll('label'))780 const label = labels.find(l => 781 l.innerText.trim().includes('Teman') || 782 l.innerText.trim().includes('Relasi') || 783 l.innerText.trim().includes('Kerabat')784 )785 if (label) { label.click(); return true }786 return false787 })788 await this.log(sourceSelected ? 'โ
Sumber dipilih' : 'โ ๏ธ Sumber tidak ditemukan')789 790 // 5m. Motivasi (textarea) - ISI INI, WAJIB!791 this.checkCancelled()792 await this.log('๐ Mengisi motivasi...')793 const motivasiFilled = await this.page.evaluate(() => {794 const textareas = document.querySelectorAll('textarea')795 for (const ta of textareas) {796 const parent = ta.closest('div')797 const label = parent?.querySelector('label')?.innerText || ''798 if (label.toLowerCase().includes('motivasi') || ta.name.toLowerCase().includes('motivasi') || 799 ta.placeholder?.toLowerCase().includes('motivasi')) {800 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set801 nativeInputValueSetter.call(ta, 'Ingin belajar teknologi Cloud AWS untuk meningkatkan skill dan kompetensi di bidang IT.')802 ta.dispatchEvent(new Event('input', { bubbles: true }))803 ta.dispatchEvent(new Event('change', { bubbles: true }))804 return true805 }806 }807 // Fallback: isi textarea pertama yang kosong808 if (textareas.length > 0) {809 const ta = textareas[0]810 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set811 nativeInputValueSetter.call(ta, 'Ingin belajar teknologi Cloud AWS untuk meningkatkan skill dan kompetensi di bidang IT.')812 ta.dispatchEvent(new Event('input', { bubbles: true }))813 ta.dispatchEvent(new Event('change', { bubbles: true }))814 return true815 }816 return false817 })818 await this.log(motivasiFilled ? 'โ
Motivasi diisi' : 'โ ๏ธ Motivasi tidak ditemukan')819 820 // 5n. Privacy policy: centang semua checkbox821 this.checkCancelled()822 await this.log('๐ Mencentang semua privacy policy...')823 const checkboxCount = await this.page.evaluate(() => {824 const checkboxes = document.querySelectorAll('input[type="checkbox"]')825 let count = 0826 for (const cb of checkboxes) {827 if (!cb.checked) {828 cb.click()829 count++830 }831 }832 return count833 })834 await this.log(`โ
${checkboxCount} checkbox dicentang`)835 836 await this.page.waitForTimeout(1000)837 838 // ===== STEP 6: Klik "Daftar Sekarang" =====839 this.checkCancelled()840 await this.log('๐ Menekan tombol "Daftar Sekarang"...')841 842 // Coba klik via ID dulu (id="registrationFormSubmitButton") 843 let submitClicked = false844 const submitById = await this.page.$('#registrationFormSubmitButton')845 if (submitById) {846 await submitById.evaluate(el => el.click()).catch(() => {})847 submitClicked = true848 } else {849 // Fallback: cari berdasarkan teks850 submitClicked = await this.page.evaluate(() => {851 const allButtons = Array.from(document.querySelectorAll('button, input[type="submit"]'))852 const daftar = allButtons.find(b => {853 const text = b.innerText || b.value || ''854 return text.toLowerCase().includes('daftar sekarang')855 })856 if (daftar) { daftar.click(); return true }857 const submit = document.querySelector('button[type="submit"]')858 if (submit) { submit.click(); return true }859 return false860 })861 }862 863 if (submitClicked) {864 await this.log('โ
Tombol Daftar Sekarang ditekan!')865 } else {866 await this.log('โ Tombol Daftar Sekarang tidak ditemukan!')867 throw new Error('Tombol submit tidak ditemukan')868 }869 870 // ===== STEP 7: Handle modal konfirmasi "Yakin, kirim formulir" =====871 await this.page.waitForTimeout(3000)872 this.checkCancelled()873 await this.log('๐ Mencari modal konfirmasi...')874 875 // Coba tunggu tombol konfirmasi muncul (via ID #submitFormDataButton)876 const confirmBtn = await this.page.waitForSelector('#submitFormDataButton', { visible: true, timeout: 5000 }).catch(() => null)877 878 if (confirmBtn) {879 await this.log('โ
Modal konfirmasi muncul! Mengklik "Yakin, kirim formulir"...')880 await confirmBtn.evaluate(el => el.click()).catch(() => {})881 await this.page.waitForTimeout(10000) // Tunggu submit selesai882 } else {883 // Fallback: cari tombol konfirmasi via teks884 const confirmClicked = await this.page.evaluate(() => {885 const buttons = Array.from(document.querySelectorAll('button'))886 const yakin = buttons.find(b => {887 const text = b.innerText.toLowerCase()888 return text.includes('yakin') && text.includes('kirim')889 })890 if (yakin) { yakin.click(); return true }891 return false892 })893 if (confirmClicked) {894 await this.log('โ
Konfirmasi ditekan via fallback')895 await this.page.waitForTimeout(10000)896 } else {897 await this.log('โ ๏ธ Modal konfirmasi tidak muncul, mungkin ada error di form')898 899 // Cek error messages di halaman900 const formErrors = await this.page.evaluate(() => {901 const errorEls = document.querySelectorAll('.invalid-feedback, .error, .text-danger, [class*="error"]')902 return Array.from(errorEls).map(e => e.innerText.trim()).filter(t => t).join('; ')903 }).catch(() => '')904 905 if (formErrors) {906 await this.log(`โ Error form: ${formErrors}`)907 }908 }909 }910 911 // ===== FINAL: Cek apakah berhasil =====912 const finalUrl = this.page.url()913 await this.log(`๐ URL akhir: ${finalUrl}`)914 915 const finalBodyText = await this.page.evaluate(() => document.body.innerText.substring(0, 1000)).catch(() => '')916 const pageTitle = await this.page.title().catch(() => 'Unknown')917 918 await this.log(`๐ Title: ${pageTitle}`)919 920 const isSuccessful = 921 finalBodyText.toLowerCase().includes('berhasil') ||922 finalBodyText.toLowerCase().includes('success') ||923 finalBodyText.toLowerCase().includes('terima kasih') ||924 finalBodyText.toLowerCase().includes('pendaftaran berhasil') ||925 finalUrl.includes('success') ||926 finalUrl.includes('dashboard') ||927 finalUrl.includes('thanks') ||928 (finalUrl.includes('aws') && 929 !finalUrl.includes('personal-data') && 930 !finalUrl.includes('registration') &&931 !finalUrl.includes('oauth') && 932 !finalUrl.includes('login'))933 934 if (isSuccessful) {935 await this.log('๐ Pendaftaran AWS BERHASIL! Selamat!')936 return { success: true, url: finalUrl, message: 'Registration AWS berhasil' }937 } else {938 await this.log(`โ ๏ธ Pendaftaran mungkin belum tuntas.`)939 await this.log(`๐ URL: ${finalUrl}`)940 941 const errorMsg = await this.page.evaluate(() => {942 const errorElements = document.querySelectorAll('.error, .alert, [class*="error"], [class*="alert"]')943 for (const el of errorElements) {944 if (el.innerText.trim()) return el.innerText.trim().substring(0, 200)945 }946 return null947 }).catch(() => null)948 949 if (errorMsg) {950 await this.log(`โ Error di halaman: ${errorMsg}`)951 return { success: false, url: finalUrl, error: `Pendaftaran gagal: ${errorMsg}` }952 }953 954 return { success: false, url: finalUrl, error: 'Pendaftaran tidak selesai - masih di halaman form' }955 }956 957 } catch (error) {958 if (error.message === 'CANCELLED' || this.isCancelled) {959 await this.log('๐ Proses dihentikan oleh pengguna.')960 return { success: false, error: 'Dibatalkan oleh pengguna', cancelled: true }961 }962 if (error.message.includes('Protocol error') || error.message.includes('Target closed') || error.message.includes('Session closed')) {963 await tgLog(this.botToken, this.chatId, '๐ <i>Proses dihentikan oleh pengguna.</i>')964 return { success: false, error: 'Dibatalkan oleh pengguna', cancelled: true }965 }966 await this.log(`โ ERROR Phase 2: ${error.message}`)967 return { success: false, error: error.message }968 } finally {969 await this.close()970 }971 }972 973 // Generate random date of birth (dd-mm-yyyy format, age 25-40)974 generateRandomBirthday() {975 const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')976 const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')977 const year = String(Math.floor(Math.random() * 16) + 1985)978 return `${day}-${month}-${year}`979 }980 981 async close() {982 if (this.browser) {983 // Bebaskan memori dari browser tab secara paksa sebelum menutup aplikasi984 if (this.page && !this.page.isClosed()) {985 await this.page.goto('about:blank').catch(() => null)986 await this.page.close().catch(() => null)987 }988 if (this.context) {989 await this.context.close().catch(() => null)990 }991 992 await new Promise(r => setTimeout(r, 500))993 994 if (!this.isSharedBrowser) {995 await this.browser.close().catch(e => console.error('Error closing browser:', e.message))996 }997 998 this.browser = null999 this.context = null1000 this.page = null1001 }1002 }1003}1004 1005module.exports = DicodingAutomator1006 