anprar/dicoding-bulk-server
0
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 