CoolFace
Apppublic

anprar/dicoding-api-bulk-pro

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
automation.js1006 linesDownload Raw Back to root
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