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);