import "dotenv/config"; import puppeteer from "puppeteer"; import fs from "fs/promises"; import path from "path"; // We can directly import our server actions because this script runs in the same project context import { saveBattleSetup } from "./app/actions/db.actions"; import { BattleSetup } from "./types"; import { generateAndSaveHero } from "./app/actions/ai.actions"; // --- CONFIGURATION --- const BASE_URL = "http://localhost:7860"; // The URL of our own Next.js app const TEMP_DIR = "/tmp"; const CYCLE_WAIT_TIME_MS = 4 * 60 * 60 * 1000; // 4 hours between cycles const BATTLE_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes timeout for battle completion // --- UTILITY --- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function sendErrorToTelegram(screenshotPath: string, errorMessage: string) { const botToken = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; if (!botToken || !chatId) { console.warn("[Telegram] Bot Token or Chat ID not configured. Skipping notification."); return; } console.log("[Telegram] An error occurred. Sending notification..."); try { // 1. Read the screenshot file from the disk (this returns a Buffer) const imageBuffer = await fs.readFile(screenshotPath); // --- FIX IS HERE --- // 2. Convert the Node.js Buffer into a standard Blob const imageBlob = new Blob([imageBuffer]); // --- END FIX --- // 3. Create the multipart form const form = new FormData(); form.append('chat_id', chatId); form.append('caption', `🤖 Automation Worker Error 🛑\n\n
${errorMessage}
`); form.append('parse_mode', 'HTML'); // 4. Append the Blob to the form data, not the Buffer // Provide a filename for the API to recognize it as a file upload. form.append('photo', imageBlob, 'error_screenshot.png'); // 5. Send the request to the Telegram API const apiUrl = `https://api.telegram.org/bot${botToken}/sendPhoto`; const response = await fetch(apiUrl, { method: 'POST', body: form, }); if (response.ok) { console.log("[Telegram] ✅ Error notification sent successfully."); } else { const errorResponse = await response.json(); console.error("[Telegram] Failed to send notification:", errorResponse); } } catch (error) { console.error("[Telegram] CRITICAL: Could not send error notification.", error); } } // --- THE MAIN AUTOMATION LOOP --- async function main() { console.log("🤖 Automation Worker Started"); console.log("⏰ Note: The shell script has implemented a 2-3 hour startup delay to avoid triggering HF monitoring"); console.log("🔄 After initial delay, cycles will run every ~4 hours with random timing variations"); console.log("🎯 Current time:", new Date().toLocaleString()); console.log("📱 Starting in organic mode..."); await sleep(5000); // Initial delay while (true) { console.log("🚀 Starting new automation cycle at", new Date().toLocaleString()); let browser; try { // 1. GENERATE HEROES console.log("[Cycle] Generating two new heroes..."); const hero1 = await generateAndSaveHero("Marvel"); await sleep(1000); // Small delay to avoid rate limits const hero2 = await generateAndSaveHero("DC"); if (!hero1 || !hero2) { throw new Error("Failed to generate one or both heroes."); } console.log( `[Cycle] Generated: ${hero1} vs ${hero2}` ); // 2. CREATE BATTLE SETUP const battleSetup: BattleSetup = { type: "HERO_BATTLE", config: { heroIds: [hero1._id as string, hero2._id as string], gameSpeed: 2.5, // Increased speed for faster battles to reduce overall CPU time activePowerUps: [ "SPEED_BOOST", "HEALTH_PACK", "GHOST", "DOMAIN_INVINCIBILITY", ], }, }; const battleId = await saveBattleSetup(battleSetup); if (!battleId) { throw new Error("Failed to save battle setup to database."); } console.log(`[Cycle] Created battle setup with ID: ${battleId}`); // 3. GENERATE ONE-TIME AUTOMATION TOKEN console.log("[Cycle] Generating one-time automation token..."); let automationToken: string | null = null; try { const tokenResponse = await fetch(`${BASE_URL}/api/youtube/automation-token`, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.AUTOMATION_SECRET_KEY}`, 'Content-Type': 'application/json', }, }); if (tokenResponse.ok) { const tokenData = await tokenResponse.json(); automationToken = tokenData.token; console.log(`[Cycle] Automation token generated: ${automationToken?.substring(0, 8)}...`); } else { throw new Error(`Token generation failed: ${tokenResponse.status}`); } } catch (error) { console.error("[Cycle] Failed to generate automation token:", error); continue; // Skip this cycle if token generation fails } if (!automationToken) { console.error("[Cycle] No automation token received, skipping cycle"); continue; } // 4. LAUNCH HEADLESS BROWSER & RECORD console.log("[Cycle] Launching headless browser..."); browser = await puppeteer.launch({ headless: true, // Use the new headless mode args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", // Important for Docker environments "--disable-web-security", // Reduce security overhead "--disable-features=VizDisplayCompositor", // Disable compositor "--disable-background-timer-throttling", // Prevent throttling "--disable-renderer-backgrounding", "--disable-backgrounding-occluded-windows", "--disable-ipc-flooding-protection", // Reduce IPC overhead "--no-zygote", // Helps in resource-constrained environments // REMOVED: --single-process to allow multi-core utilization "--process-per-site", // Use separate processes per site for better CPU distribution "--max_old_space_size=512", // Increased from 256MB since we have 16GB RAM "--memory-pressure-off", // Disable memory pressure notifications "--autoplay-policy=no-user-gesture-required", "--use-fake-ui-for-media-stream", // Optimized window size for HF free tier (540x960 instead of 1080x1920) "--window-size=540,960", ], }); const page = await browser.newPage(); await page.setViewport({ width: 540, height: 960 }); // Optimized resolution for free tier // **** SIMPLIFIED: No need for file transfer since we upload directly from browser **** console.log("[WORKER] Browser automation simplified - no file transfer needed"); // Set up browser console logging page.on("console", (msg) => { const type = msg.type(); const text = msg.text(); console.log(`[Browser Console - ${type.toUpperCase()}]: ${text}`); }); page.on("pageerror", (error) => { console.error(`[Browser Page Error]: ${error.message}`); }); page.on("requestfailed", (request) => { console.warn( `[Browser Request Failed]: ${request.url()} - ${ request.failure()?.errorText }` ); }); // 5. Build battle URL with automation token const automationParams = new URLSearchParams({ automation: "true", token: automationToken, }); const battleUrl = `${BASE_URL}/battle/${battleId}?${automationParams.toString()}`; console.log(`[Cycle] Battle URL: ${battleUrl.replace(automationToken, automationToken.substring(0, 8) + '...')}`); await page.goto(battleUrl, { waitUntil: "networkidle0" }); // --- SIMPLIFIED: Just wait for battle completion --- console.log("[Cycle] Battle is running... waiting for completion and direct upload from browser."); console.log("[Cycle] Expected flow: Battle ends → Recording stops → Upload completes → Window closes"); // Wait for battle completion (automation will upload directly from browser) const battleTimeout = Date.now() + BATTLE_TIMEOUT_MS; let battleComplete = false; // Look for success indicators in browser console or page closure while (Date.now() < battleTimeout && !battleComplete) { try { // Check if we can detect completion via page evaluation const isComplete = await page.evaluate(() => { // Look for toast success messages or other completion indicators const toastElements = document.querySelectorAll('[data-sonner-toast]'); for (const toast of toastElements) { const text = toast.textContent || ''; if (text.includes('Automation upload complete') || text.includes('Upload Complete')) { return true; } } return false; }); if (isComplete) { console.log("[Cycle] ✅ Battle completed successfully! Upload handled by browser."); battleComplete = true; break; } // Check if page is still responsive (not closed) const pageTitle = await page.title().catch(() => null); if (!pageTitle) { console.log("[Cycle] ✅ Page closed - battle completed!"); battleComplete = true; break; } await sleep(3000); // Check every 3 seconds (reduced from 5) } catch (error) { console.error("[Cycle] Error checking battle status:", error); // If we can't access the page, it might have closed console.log("[Cycle] ✅ Page appears to be closed - assuming battle completed"); battleComplete = true; break; } } if (!battleComplete) { // Capture a screenshot for debugging if timeout occurs const screenshotPath = path.join(TEMP_DIR, "timeout_screenshot.png"); const timeoutMessage = "Battle timed out. No completion detected within timeout period."; try { await page.screenshot({ path: screenshotPath as `${string}.png`, fullPage: true }); console.log(`[Cycle] Timeout screenshot saved to ${screenshotPath}`); await sendErrorToTelegram(screenshotPath, timeoutMessage); } catch (screenshotError) { console.error(`[Cycle] Failed to take timeout screenshot: ${screenshotError}`); } throw new Error(timeoutMessage); } console.log("[Cycle] Closing browser."); await browser.close(); browser = undefined; // Force garbage collection for memory optimization on HF free tier if (global.gc) { console.log("[Cycle] Running garbage collection..."); global.gc(); } console.log("[Cycle] Battle cycle completed successfully!"); } catch (error) { console.error("⛔ AUTOMATION CYCLE FAILED ⛔"); console.error(error); if (browser) { try { const pages = await browser.pages(); if (pages.length > 0) { const currentPage = pages[0]; const errorScreenshotPath = path.join(TEMP_DIR, 'error_screenshot.png'); await currentPage.screenshot({ path: errorScreenshotPath as `${string}.png`, fullPage: true }); console.log(`[Cycle] Error screenshot saved to ${errorScreenshotPath}`); if (error instanceof Error) { await sendErrorToTelegram(errorScreenshotPath, error.message); } } } catch (screenshotError) { console.error(`[Cycle] Failed to take error screenshot: ${screenshotError}`); } console.log("[Cycle] Closing browser after error..."); await browser.close(); browser = undefined; } // Force garbage collection after any error to free memory if (global.gc) { console.log("[Cycle] Running post-error garbage collection..."); global.gc(); } } // Add randomization to cycle timing to appear more organic to HF monitoring // Base: 4 hours ± 30 minutes random variation const randomVariation = (Math.random() - 0.5) * 30 * 60 * 1000; // ±30 minutes in ms const actualWaitTime = CYCLE_WAIT_TIME_MS + randomVariation; const waitMinutes = Math.round(actualWaitTime / 1000 / 60); console.log( `[Cycle] Cycle complete. Waiting for ${waitMinutes} minutes (${Math.round(actualWaitTime/1000/60/60 * 10)/10}h) with organic timing variation...` ); console.log(`[Cycle] Next cycle expected at: ${new Date(Date.now() + actualWaitTime).toLocaleString()}`); await sleep(actualWaitTime); } } main().catch(console.error);