CoolFace
Apppublic

anprar/dicoding-bulk-server

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
automation-server.js590 linesDownload Raw Back to root
1// src/automation-server.js - Standalone automation server (run separately from Cloudflare Workers on Render/Railway)2// This server handles browser automation with Puppeteer Stealth3// Run with: node src/automation-server.js4 5const express = require('express')6const cors = require('cors')7const DicodingAutomator = require('./automation')8 9const app = express()10 11// Middleware12app.use(express.json())13app.use(cors())14 15const PORT = process.env.PORT || 300016 17// Global map to track active automator instances per chatId18// Key: chatId (string), Value: DicodingAutomator instance19const activeSessions = new Map()20 21// Phase 1: Register Account on Dicoding Main Site22// --- GENERATORS FOR BULK MODE ---23function generateRandomPhone() {24  const prefixes = ['812', '813', '821', '822', '852', '853', '811', '814', '815', '816', '817', '818', '819', '823', '828', '831', '832', '833', '838', '851', '855', '856', '857', '858', '877', '878', '895', '896', '897', '898', '899']25  return prefixes[Math.floor(Math.random() * prefixes.length)] + Math.floor(Math.random() * 90000000 + 10000000).toString()26}27 28function generateRandomName() {29  const firstNames = [30    'Budi', 'Siti', 'Agus', 'Dewi', 'Eko', 'Rina', 'Hendra', 'Nita', 'Yoga', 'Putri', 'Andi', 'Maya', 'Rizki', 'Ani', 'Dimas', 'Reza', 'Tari', 'Ahmad', 'Nurul', 'Arif', 'Dian', 'Wahyu', 'Siska', 'Aditya', 'Ayu', 'Iqbal', 'Santi', 'Deni', 'Indah', 'Irfan', 'Sri', 'Aldo', 'Tika', 'Gilang', 'Mega', 'Bayu', 'Dina', 'Fikri', 'Lia', 'Galih', 'Rini', 'Surya', 'Ratih', 'Kevin', 'Citra', 'Faisal', 'Ratna', 'Arya', 'Wulan', 'Dika', 'Sari', 'Ilham', 'Nadia', 'Dodi', 'Desi', 'Rangga', 'Nisa', 'Oki', 'Vina', 'Guntur', 'Hani', 'Fajar', 'Yuni', 'Doni', 'Sari', 'Bagas', 'Rika', 'Heru', 'Sinta', 'Rian', 'Purnama', 'Bima', 'Mita', 'Ivan', 'Vera', 'Iwan', 'Reni', 'Yusuf', 'Eka', 'Aulia', 'Amelia', 'Bintang', 'Fitri', 'Arief', 'Tina', 'Teguh', 'Anita', 'Rama', 'Wati', 'Joko'31  ]32  const lastNames = [33    'Wibowo', 'Sari', 'Pratama', 'Rahayu', 'Santoso', 'Handayani', 'Setiawan', 'Kusuma', 'Wijaya', 'Utami', 'Hidayat', 'Susanti', 'Firmansyah', 'Permata', 'Nugroho', 'Putra', 'Putri', 'Anggara', 'Syahputra', 'Maulana', 'Lestari', 'Kurniawan', 'Ramadhan', 'Saputra', 'Cahyono', 'Baskoro', 'Pamungkas', 'Siregar', 'Mulyana', 'Purwanto', 'Raharjo', 'Pangestu', 'Suharto', 'Fauzi', 'Hakim', 'Nasution', 'Hadi', 'Sanjaya', 'Hartono', 'Gunawan', 'Sutejo', 'Hermansyah', 'Rinaldi', 'Lubis', 'Kurnia', 'Yulianto', 'Syafiq', 'Sugiarto', 'Wirawan', 'Wibisono', 'Kusnadi', 'Sinaga', 'Simanjuntak', 'Mustofa', 'Syaifullah', 'Prasetyo', 'Wardana', 'Haryanto', 'Ismail', 'Irawan', 'Akbar', 'Suryono', 'Halim', 'Tanjung', 'Laksana', 'Kamil', 'Mahardika', 'Prakoso', 'Kuswandari', 'Ardiansyah', 'Purnomo', 'Dharma', 'Sulistyo'34  ]35  return `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${lastNames[Math.floor(Math.random() * lastNames.length)]}`36}37 38function generateRandomCity() {39  const cities = ['Jakarta', 'Surabaya', 'Bandung', 'Semarang', 'Yogyakarta', 'Medan', 'Makassar', 'Palembang', 'Denpasar', 'Balikpapan', 'Manado', 'Pontianak', 'Banjarmasin', 'Padang', 'Malang', 'Bogor', 'Bekasi', 'Depok', 'Tangerang', 'Samarinda']40  return cities[Math.floor(Math.random() * cities.length)]41}42 43// Safe JSON parser — prevents "Unexpected end of JSON input" crashes44async function safeJsonParse(response) {45  const text = await response.text()46  if (!text || text.trim().length === 0) throw new Error(`Empty response (HTTP ${response.status})`)47  try { return JSON.parse(text) } catch(e) { throw new Error(`Invalid JSON (HTTP ${response.status}): ${text.substring(0,100)}`) }48}49 50// Provider 1: mail.tm (PRIMARY — domain deltajohnsons.com menerima email Dicoding)51async function createMailTmEmail() {52  const dRes = await fetch('https://api.mail.tm/domains')53  if (!dRes.ok) throw new Error(`mail.tm domains HTTP ${dRes.status}`)54  const dJson = await safeJsonParse(dRes)55  if (!dJson['hydra:member'] || dJson['hydra:member'].length === 0) throw new Error('mail.tm no domains available')56  57  const domain = dJson['hydra:member'][0].domain58  const email = 'bot' + Math.floor(Math.random() * 9000000 + 1000000) + '@' + domain59  const mailPass = 'AwsDicodingSecret123!'60  61  const accRes = await fetch('https://api.mail.tm/accounts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({address: email, password: mailPass}) })62  const accJson = await safeJsonParse(accRes)63  if (!accRes.ok) throw new Error(accJson.message || 'Failed creating mail.tm account')64  65  const tRes = await fetch('https://api.mail.tm/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({address: email, password: mailPass}) })66  if (!tRes.ok) throw new Error('Failed getting mail.tm token')67  const tJson = await safeJsonParse(tRes)68  if (!tJson.token) throw new Error('mail.tm token missing in response')69  70  return { email, mailToken: tJson.token, providerName: 'mail.tm', mailAccountId: accJson.id }71}72 73// Provider 2: GuerrillaMail (FALLBACK - Highly reliable API, bypasses CF)74async function createGuerrillaMailEmail() {75  const tRes = await fetch('https://api.guerrillamail.com/ajax.php?f=get_email_address')76  if (!tRes.ok) throw new Error(`GuerrillaMail HTTP ${tRes.status}`)77  const tJson = await safeJsonParse(tRes)78  if (!tJson.email_addr || !tJson.sid_token) throw new Error('GuerrillaMail missing address/token')79  80  // GuerrillaMail provides 'sharklasers.com' as an alias domain which bypasses blocklists well.81  const emailAlias = tJson.email_addr.split('@')[0] + '@sharklasers.com'82  83  return { 84    email: emailAlias, 85    mailToken: tJson.sid_token, 86    providerName: 'GuerrillaMail', 87    mailAccountId: '' 88  }89}90 91// Smart Dual-Route: Try mail.tm first, fallback to GuerrillaMail92async function prepareRandomEmail() {93  try {94    console.log('[Email] Trying mail.tm (primary)...')95    return await createMailTmEmail()96  } catch(e1) {97    console.log('[Email] mail.tm failed:', e1.message, '— falling back to GuerrillaMail')98    try {99      return await createGuerrillaMailEmail()100    } catch(e2) {101      console.log('[Email] GuerrillaMail also failed:', e2.message)102      throw new Error(`Kedua provider gagal. mail.tm: ${e1.message} | GuerrillaMail: ${e2.message}`)103    }104  }105}106 107// Health Check Endpoint (Sangat Penting untuk Hugging Face)108app.get('/', (req, res) => {109  res.status(200).send('Bot Server is Running and Healthy!')110})111 112app.get('/health', (req, res) => {113  res.status(200).send('OK')114})115 116// ------------------------------117app.post('/register-phase1', async (req, res) => {118  const { name, email, password, phone, botToken, chatId } = req.body119 120  if (!email || !password || !name) {121    return res.status(400).json({122      success: false,123      error: 'Missing required fields (name, email, password)'124    })125  }126 127  // Kill any existing session for this chatId before starting a new one128  if (chatId && activeSessions.has(chatId)) {129    const old = activeSessions.get(chatId)130    await old.cancel().catch(() => {})131    activeSessions.delete(chatId)132  }133 134  const automator = new DicodingAutomator(botToken, chatId)135 136  // Register active session137  if (chatId) activeSessions.set(chatId, automator)138 139  try {140    const result = await automator.registerPhase1({ name, email, password, phone })141    // Always delete the session tracking as the browser is now closed completely142    if (chatId) activeSessions.delete(chatId)143    res.json(result)144  } catch (error) {145    if (chatId) activeSessions.delete(chatId)146    res.status(500).json({147      success: false,148      error: error.message149    })150  }151})152 153// Helper: send Telegram message directly from Render (for background tasks)154async function sendTelegramDirect(botToken, chatId, text) {155  if (!botToken || !chatId) return156  try {157    await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {158      method: 'POST',159      headers: { 'Content-Type': 'application/json' },160      body: JSON.stringify({161        chat_id: chatId,162        text: text,163        parse_mode: 'HTML',164        reply_markup: { remove_keyboard: true }165      })166    })167  } catch (e) {168    console.error('[TG Direct] Failed to send:', e.message)169  }170}171 172// Phase 2: Login and Fill AWS Specific Data (After Manual Email Verification)173// FIRE-AND-FORGET: responds immediately to Worker, runs automation in background174app.post('/register-phase2', async (req, res) => {175  const { email, password, cityId, phone, botToken, chatId } = req.body176 177  if (!email || !password) {178    return res.status(400).json({179      success: false,180      error: 'Missing required fields (email, password)'181    })182  }183 184  // ALWAYS Create new clean, incognito session for Phase 2185  // We first kill any old active sessions to be absolutely safe186  if (chatId && activeSessions.has(chatId)) {187    const s = activeSessions.get(chatId)188    await s.cancel().catch(() => {})189    activeSessions.delete(chatId)190  }191 192  const automator = new DicodingAutomator(botToken, chatId)193  if (chatId) activeSessions.set(chatId, automator)194 195  // RESPOND IMMEDIATELY to prevent Cloudflare Worker timeout196  res.json({ success: true, message: 'Phase 2 started in background', background: true })197 198  // RUN AUTOMATION IN BACKGROUND - results sent directly to Telegram199  automator.registerPhase2({200    email,201    password,202    cityId: cityId || '1',203    phone204  }).then(async (result) => {205    if (result.success) {206      await sendTelegramDirect(botToken, chatId,207        '🎉 <b>PENDAFTARAN AWS SELESAI!</b>\n\n' +208        'Semua data berhasil diinput. Selamat belajar di Dicoding!'209      )210    } else if (!result.cancelled) {211      await sendTelegramDirect(botToken, chatId,212        '❌ <b>Pendaftaran AWS Gagal!</b>\n\n' +213        `Error: ${result.error || 'Unknown'}\n\n` +214        'Silakan ketik /verify untuk mencoba lagi.'215      )216    }217  }).catch(async (error) => {218    await sendTelegramDirect(botToken, chatId,219      '❌ <b>Terjadi kesalahan!</b>\n\n' +220      `Error: ${error.message}\n\n` +221      'Silakan ketik /verify untuk mencoba lagi.'222    )223  }).finally(() => {224    if (chatId) activeSessions.delete(chatId)225  })226})227 228// Cancel endpoint: Kill active Puppeteer session for a given chatId229app.post('/cancel', async (req, res) => {230  const { chatId } = req.body231 232  if (!chatId) {233    return res.status(400).json({ success: false, error: 'Missing chatId' })234  }235 236  if (activeSessions.has(chatId)) {237    const automator = activeSessions.get(chatId)238    await automator.cancel().catch(() => {})239    activeSessions.delete(chatId)240    console.log(`[Cancel] Session for chatId ${chatId} has been killed.`)241    return res.json({ success: true, message: 'Session cancelled successfully' })242  } else {243    return res.json({ success: false, message: 'No active session found for this chatId' })244  }245})246 247app.get('/health', (req, res) => {248  res.json({ 249    status: 'ok', 250    timestamp: new Date().toISOString(),251    activeSessions: activeSessions.size252  })253})254 255// Final: Helper to check Mail.tm and Extract Verify Link256async function pollMailTm(mailToken) {257  const maxRetries = 24 // 2 minutes (every 5 seconds)258  259  for (let i = 0; i < maxRetries; i++) {260    try {261      const res = await fetch('https://api.mail.tm/messages', {262        headers: { 'Authorization': `Bearer ${mailToken}` }263      })264      const json = await res.json()265      266      if (json['hydra:totalItems'] > 0) {267        // Look through emails for dicoding verification268        for (const mail of json['hydra:member']) {269          if (mail.from.address.includes('dicoding') || mail.subject.toLowerCase().includes('verifikasi')) {270            // Found the email reference! We need to fetch the full mail.271            const fullRes = await fetch(`https://api.mail.tm/messages/${mail.id}`, {272              headers: { 'Authorization': `Bearer ${mailToken}` }273            })274            const fullJson = await fullRes.json()275            276            // Extract URL from HTML content277            const content = fullJson.html[0] || fullJson.text || ''278            const regex = /https:\/\/www\.dicoding\.com\/usermailverification\/[^\s"'>]+/g279            const matches = content.match(regex)280            if (matches && matches.length > 0) {281              return matches[0]282            }283          }284        }285      }286    } catch(e) {287      console.log('Polling Mail.tm error:', e.message)288    }289    290    // Wait 5 seconds291    await new Promise(r => setTimeout(r, 5000))292  }293  return null294}295 296// Final: Helper to check GuerrillaMail and Extract Verify Link297async function pollGuerrillaMail(mailToken) {298  const maxRetries = 24 // 2 minutes (every 5 seconds)299  300  for (let i = 0; i < maxRetries; i++) {301    try {302      const res = await fetch(`https://api.guerrillamail.com/ajax.php?f=check_email&seq=0&sid_token=${mailToken}`)303      const json = await safeJsonParse(res)304      305      if (json.list && json.list.length > 0) {306        for (const mail of json.list) {307          // GuerrillaMail includes 'Welcome' message, so we strictly look for Dicoding308          const subject = (mail.mail_subject || '').toLowerCase()309          const from = (mail.mail_from || '').toLowerCase()310          311          if (from.includes('dicoding') || subject.includes('verifikasi')) {312            // Usually check_email returns mail_excerpt, we need to fetch the full mail313            const msgRes = await fetch(`https://api.guerrillamail.com/ajax.php?f=fetch_email&email_id=${mail.mail_id}&sid_token=${mailToken}`)314            const msgJson = await safeJsonParse(msgRes)315            const content = msgJson.mail_body || ''316 317            const regex = /https:\/\/www\.dicoding\.com\/usermailverification\/[^\s"'>]+/g318            const matches = content.match(regex)319            if (matches && matches.length > 0) {320              return matches[0]321            }322          }323        }324      }325    } catch(e) {326      console.log('Polling GuerrillaMail error:', e.message)327    }328    329    await new Promise(r => setTimeout(r, 5000))330  }331  return null332}333 334// Phase 3: FULL AUTO-PILOT (Phase 1 -> Polling Email -> Verification -> Phase 2)335// This endpoint replaces the old auto-register-full. It handles 1 identity, self-generated.336app.post('/auto-register-single', async (req, res) => {337  const { botToken, chatId } = req.body338 339  // FIRE AND FORGET340  res.json({ success: true, message: 'Auto-pilot single process started in background' })341 342  // BEGIN BACKGROUND TASK343  ;(async () => {344    // Handle active session overriding345    if (chatId && activeSessions.has(chatId)) {346      const s = activeSessions.get(chatId)347      await s.cancel().catch(() => {})348      activeSessions.delete(chatId)349    }350 351    const automator = new DicodingAutomator(botToken, chatId)352    if (chatId) activeSessions.set(chatId, automator)353 354    // Generate credentials natively in Node!355    const password = 'AwsDicoding' + Math.floor(Math.random() * 9000 + 1000) + '!'356    const name = generateRandomName()357    const phone = generateRandomPhone()358    const cityId = '1'359 360    await automator.log('⏳ Sedang menyiapkan Kredensial dan Jalur Email Sementara...', true)361    362    let mailData = null363    try {364      mailData = await prepareRandomEmail()365    } catch(err) {366      await sendTelegramDirect(botToken, chatId, `❌ <b>Gagal membuat Email Temp:</b> ${err.message}`)367      if (chatId) activeSessions.delete(chatId)368      return369    }370 371    await automator.log(`🤖 <b>Kredensial Siap (via ${mailData.providerName})</b>\n\nNama: <code>${name}</code>\nEmail: <code>${mailData.email}</code>\nSandi: <code>${password}</code>\n\nSedang memulai Phase 1...`, true)372 373    try {374      // 1. Run Phase 1375      const p1Result = await automator.registerPhase1({ name, email: mailData.email, password, phone })376    if (!p1Result.success) {377      await sendTelegramDirect(botToken, chatId, `❌ <b>Auto-Pilot Gagal di Tahap 1.</b>\n${p1Result.error || 'Server error'}`)378      if (chatId) activeSessions.delete(chatId)379      return380    }381 382      // 2. Poll Email383      const providerStr = mailData.providerName384      await automator.log(`⏳ Menunggu Verifikasi Email dari ${providerStr} (Maksimal 2 Menit)...`, true)385      386      // Kita hapus browser dari Phase 1 dulu (clear ram) karena kita akan nunggu agak lama387      await automator.close()388      389      let verificationUrl = null390      if (providerStr === 'GuerrillaMail') {391        verificationUrl = await pollGuerrillaMail(mailData.mailToken)392      } else {393        verificationUrl = await pollMailTm(mailData.mailToken)394      }395      396      if (!verificationUrl) {397        await sendTelegramDirect(botToken, chatId, `❌ <b>Auto-Pilot Timeout.</b>\nEmail verifikasi dari Dicoding tidak kunjung masuk ke penyedia ${providerStr} selama 2 menit.`)398        if (chatId) activeSessions.delete(chatId)399        return400      }401 402      await automator.log('✅ Email verifikasi ditangkap! Mengawali Bypass Verifikasi & eksekusi Phase 2...', true)403 404      // 3. Run Verify & Phase 2 combined (New Automator Method)405      const p2Result = await automator.registerPhase2({ email: mailData.email, password, phone, cityId }, verificationUrl)406      407      if (p2Result.success) {408        await sendTelegramDirect(botToken, chatId, 409          '🎉 <b>BERHASIL! AKUN AWS 1-CLICK TUNTAS.</b>\n\n' +410          `👤 Nama: ${name}\n` +411          `📧 Email: <code>${mailData.email}</code>\n` +412          `🔐 Sandi: <code>${password}</code>\n\n` +413          'Silakan simpan identitas ini. Anda sudah bisa langsung Login AWS!'414        )415      } else {416        await sendTelegramDirect(botToken, chatId, `❌ <b>Auto-Pilot Phase 2 Gagal.</b>\n${p2Result.error || 'Unknown'}\nNamun akun Anda sudah terverifikasi dan bisa dilanjutkan menggunakan opsi Manual.`)417      }418 419      await automator.log('✅ Pendaftaran AWS tuntas dinavigasi!', true)420 421    } catch(error) {422      await sendTelegramDirect(botToken, chatId, `❌ <b>Auto-Pilot Error.</b>\nTerjadi kesalahan eksekusi: ${error.message}`)423    } finally {424      await automator.close().catch(()=>{})425      if (chatId) activeSessions.delete(chatId)426    }427  })()428})429 430// Phase 4: BULK AUTO-PILOT (Sequential execution of multiple identities via Node Infinite Loop)431app.post('/auto-register-bulk', async (req, res) => {432  const { targetCount, botToken, chatId } = req.body433  const numericCount = parseInt(targetCount)434 435  if (!numericCount || numericCount <= 0) {436    return res.status(400).json({ success: false })437  }438 439  // FIRE AND FORGET440  res.json({ success: true, message: 'Bulk auto-pilot process started in background' })441 442  // BACKGROUND TASK: Run the Factory Loop asynchronously443  ;(async () => {444    let successful = 0445    let consecutiveFailures = 0446    const maxFailures = 5 // Toleransi lebih tinggi sebelum menyerah447 448    // Kill existing session safety449    if (chatId && activeSessions.has(chatId)) {450      const s = activeSessions.get(chatId)451      await s.cancel().catch(() => {})452      activeSessions.delete(chatId)453    }454 455    // Helper: Force kill ALL leftover chromium/chrome processes to reclaim RAM456    async function forceCleanupMemory() {457      try {458        const { execSync } = require('child_process')459        // Kill any orphaned chromium processes460        try { execSync('pkill -f chromium || true', { timeout: 5000 }) } catch(e) {}461        try { execSync('pkill -f chrome || true', { timeout: 5000 }) } catch(e) {}462        // Force Node garbage collection if available463        if (global.gc) { global.gc() }464      } catch(e) {465        console.log('[Bulk] Cleanup warning:', e.message)466      }467    }468 469    try {470      while (successful < numericCount && consecutiveFailures < maxFailures) {471        const iterationNum = successful + 1472        const password = 'AwsDicoding' + Math.floor(Math.random() * 9000 + 1000) + '!'473        const name = generateRandomName()474        const phone = generateRandomPhone()475        const cityId = '1'476 477        await sendTelegramDirect(botToken, chatId, `⚙️ <b>[BULK ${iterationNum}/${numericCount}]</b> (Gagal berturut: ${consecutiveFailures}/${maxFailures})\nSedang menyiapkan email...`)478 479        let mailData = null480        try {481          mailData = await prepareRandomEmail()482        } catch(err) {483          consecutiveFailures++484          await sendTelegramDirect(botToken, chatId, `⚠️ Gagal membuat email: ${err.message}. Retry...`)485          await new Promise(r => setTimeout(r, 3000))486          continue487        }488 489        await sendTelegramDirect(botToken, chatId, `🏭 <b>Eksekusi Akun ${iterationNum}/${numericCount}</b>\n\nNama: <code>${name}</code>\nEmail (${mailData.providerName}): <code>${mailData.email}</code>\nSandi: <code>${password}</code>`)490 491        let automator = null492        try {493          // === PHASE 1 ===494          // Instantiate a completely new automator and browser for isolation matching single mode495          automator = new DicodingAutomator(botToken, chatId)496          if (chatId) activeSessions.set(chatId, automator)497 498          const p1Result = await automator.registerPhase1({ name, email: mailData.email, password, phone })499          if (!p1Result.success) throw new Error(p1Result.error || 'Phase 1 Failed')500 501          // Tutup browser dari Phase 1 (bebas RAM penuh)502          await automator.close().catch(() => {})503          if (chatId) activeSessions.delete(chatId)504          await forceCleanupMemory()505 506          // === EMAIL POLLING (no browser needed) ===507          let verificationUrl = null508          if (mailData.providerName === 'GuerrillaMail') {509            verificationUrl = await pollGuerrillaMail(mailData.mailToken)510          } else {511            verificationUrl = await pollMailTm(mailData.mailToken)512          }513 514          if (!verificationUrl) throw new Error(`Timeout email dari ${mailData.providerName}`)515 516          // === PHASE 2 ===517          // The same automator will launch a fresh browser instance internally via init()518          if (chatId) activeSessions.set(chatId, automator)519 520          const p2Result = await automator.registerPhase2({ email: mailData.email, password, phone, cityId }, verificationUrl)521          if (!p2Result.success) throw new Error(p2Result.error || 'Phase 2 Failed')522 523          // SUCCESS!524          successful++525          consecutiveFailures = 0526          await sendTelegramDirect(botToken, chatId, `✅ <b>BERHASIL (${successful}/${numericCount})</b>\n\n🆔 Nama: <code>${name}</code>\n📧 Email: <code>${mailData.email}</code>\n🔑 Sandi: <code>${password}</code>\n🌐 Provider: ${mailData.providerName}`)527        } catch (err) {528          consecutiveFailures++529          await sendTelegramDirect(botToken, chatId, `⚠️ <b>Gagal ${iterationNum}/${numericCount}</b>\n${err.message}\n<i>Retry (${consecutiveFailures}/${maxFailures})...</i>`)530        } finally {531          // KRITIS: Pastikan browser SELALU ditutup dan proses zombie dibunuh532          if (automator) {533            await automator.close().catch(() => {})534            automator = null535          }536          if (chatId) activeSessions.delete(chatId)537          await forceCleanupMemory()538        }539 540        // Cooldown 10 detik antar iterasi — beri OS waktu reclaim RAM541        await sendTelegramDirect(botToken, chatId, `🧹 <i>Cooldown 10 detik — membersihkan RAM...</i>`)542        await new Promise(r => setTimeout(r, 10000))543      }544    } catch(loopCrash) {545      await sendTelegramDirect(botToken, chatId, `💥 <b>BULK CRASH:</b> ${loopCrash.message}\nBerhasil sebelum crash: ${successful}/${numericCount}`)546    }547 548    if (successful >= numericCount) {549      await sendTelegramDirect(botToken, chatId, `🎉 <b>BULK SELESAI!</b>\nTarget ${numericCount} akun berhasil sepenuhnya.`)550    } else if (consecutiveFailures >= maxFailures) {551      await sendTelegramDirect(botToken, chatId, `🛑 <b>BULK TERHENTI</b>\nGagal ${maxFailures}x berturut. Berhasil: ${successful}/${numericCount} akun.`)552    }553 554    // Akhiri proses dan bersihkan RAM555    await forceCleanupMemory()556  })()557})558 559// ====== CRITICAL: Prevent silent crashes that cause HF to pause ======560process.on('uncaughtException', (err) => {561  console.error('[FATAL] Uncaught Exception:', err.message)562  console.error(err.stack)563  // Do NOT exit — keep the server alive564})565 566process.on('unhandledRejection', (reason, promise) => {567  console.error('[FATAL] Unhandled Rejection at:', promise, 'reason:', reason)568  // Do NOT exit — keep the server alive569})570 571app.listen(PORT, '0.0.0.0', () => {572  console.log(`Automation server running on port ${PORT}`)573  console.log(`Health check: http://localhost:${PORT}/health`)574  575  // SELF-PING: Buat HTTP request ke diri sendiri setiap 4 menit576  // Ini mencegah HF Spaces menganggap server "tidak aktif" dan mem-pause-nya577  const http = require('http')578  setInterval(() => {579    const mem = process.memoryUsage()580    console.log(`[Keep-Alive] RSS: ${Math.round(mem.rss / 1024 / 1024)}MB | Heap: ${Math.round(mem.heapUsed / 1024 / 1024)}/${Math.round(mem.heapTotal / 1024 / 1024)}MB | Uptime: ${Math.round(process.uptime())}s`)581    582    // Ping diri sendiri via HTTP agar HF mendeteksi ada traffic583    http.get(`http://localhost:${PORT}/health`, (res) => {584      console.log(`[Self-Ping] OK (${res.statusCode})`)585    }).on('error', (err) => {586      console.log(`[Self-Ping] Error: ${err.message}`)587    })588  }, 4 * 60 * 1000) // Setiap 4 menit589})590