const axios = require('axios'); const cheerio = require('cheerio'); const fs = require('fs').promises; const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/'; const TESTFLIGHT_IDS = [ 'pLmKZJKw', // tiktok 'An0RiOFF' // cash app ]; const STATUS_FILE = 'testflight_status.json'; function parseTestFlightID(testFlightInput) { return testFlightInput // Remove any query .replace(/\?.*/, "") // Remove any trailing slash .replace(/\/$/, "") // Split URL by slashes .split("/") // Get last item (ID) in split URL .slice(-1)[0]; } function buildTestFlightURL(testFlightID) { return "https://testflight.apple.com/join/" + testFlightID; } async function checkTestFlightStatus(testFlightID) { const testFlightURL = buildTestFlightURL(testFlightID); try { console.log(`[system] Checking TestFlight app: ${testFlightID}`); // Make HTTP GET request to TestFlight page const response = await axios.get(testFlightURL, { headers: { "Accept-Language": "en-us" }, timeout: 15000 }); const $ = cheerio.load(response.data); // Extract status information const statusElement = $('.beta-status'); if (!statusElement.length) { return { id: testFlightID, url: testFlightURL, status: 'unknown', error: 'Status element not found', timestamp: new Date().toISOString() }; } const statusText = statusElement.find('span').first().text().trim(); if (!statusText) { return { id: testFlightID, url: testFlightURL, status: 'unknown', error: 'Status text not found', timestamp: new Date().toISOString() }; } // Determine status based on text content let status; if (statusText === "This beta is full.") { status = "full"; } else if (statusText.startsWith("This beta isn't accepting")) { status = "closed"; } else { status = "open"; } // Extract app icon URL if available let iconURL = null; if (status !== "closed") { const appIconElement = $('.app-icon'); if (appIconElement.length) { const backgroundImage = appIconElement.css('background-image'); if (backgroundImage && backgroundImage !== 'none') { iconURL = backgroundImage .replace(/^url\(["']?/, '') .replace(/["']?\)$/, ''); } } } let appName = null; const pageTitle = $('title').text().trim(); if (pageTitle) { // Remove "Join the " prefix and " beta - TestFlight - Apple" suffix appName = pageTitle .replace(/^Join the /, '') // Remove "Join the " from the beginning .replace(/ beta - TestFlight - Apple$/, ''); // Remove " beta - TestFlight - Apple" from the end } return { id: testFlightID, url: testFlightURL, status, iconURL, appName, statusText, timestamp: new Date().toISOString() }; } catch (error) { console.error(`[error] Error checking TestFlight ${testFlightID}:`, error.message); return { id: testFlightID, url: testFlightURL, status: 'error', error: error.message, timestamp: new Date().toISOString() }; } } // Send Discord webhook notification async function sendDiscordNotification(appData, isStatusChange = false) { const statusEmojis = { 'open': 'โœ…', 'full': 'โš ๏ธ', 'closed': 'โŒ', 'error': '๐Ÿ”ด', 'unknown': 'โ“' }; const statusColors = { 'open': 0x00ff00, // Green 'full': 0xffff00, // Yellow 'closed': 0xff0000, // Red 'error': 0x800080, // Purple 'unknown': 0x808080 // Gray }; const embed = { title: `${statusEmojis[appData.status]} TestFlight Status${isStatusChange ? ' Change' : ''}`, description: `**App:** ${appData.appName || 'Unknown App'}\n**Status:** ${appData.status.toUpperCase()}`, color: statusColors[appData.status] || statusColors.unknown, fields: [ { name: 'TestFlight ID', value: appData.id, inline: true }, { name: 'Status', value: appData.statusText || appData.status, inline: true }, { name: 'URL', value: `[Open TestFlight](${appData.url})`, inline: true } ], timestamp: appData.timestamp, footer: { text: 'TestFlight Monitor' } }; if (appData.iconURL) { embed.thumbnail = { url: appData.iconURL }; } const payload = { embeds: [embed] }; try { const response = await axios.post(DISCORD_WEBHOOK_URL, payload, { headers: { 'Content-Type': 'application/json' } }); console.log(`[system] Discord notification sent for ${appData.id} (${appData.status}) - Response: ${response.status}`); } catch (error) { console.error(`[error] Failed to send Discord notification for ${appData.id}:`, error.message); if (error.response) { console.error(`[error] Discord API Response:`, error.response.status, error.response.data); } if (error.request) { console.error(`[error] Request failed:`, error.request); } } } // Load previous status from file async function loadPreviousStatus() { try { const data = await fs.readFile(STATUS_FILE, 'utf8'); return JSON.parse(data); } catch (error) { console.log('[info] No previous status file found, starting fresh'); return {}; } } // Save current status to file async function saveCurrentStatus(statusData) { try { await fs.writeFile(STATUS_FILE, JSON.stringify(statusData, null, 2)); } catch (error) { console.error('[error] Failed to save status file:', error.message); } } async function monitorTestFlightApps() { if (TESTFLIGHT_IDS.length === 0) { console.log('[error] No TestFlight IDs configured. Please add TestFlight IDs to the TESTFLIGHT_IDS array.'); return; } console.log(`[system] Starting TestFlight monitoring for ${TESTFLIGHT_IDS.length} apps...`); try { const previousStatus = await loadPreviousStatus(); const currentStatus = {}; for (const testFlightID of TESTFLIGHT_IDS) { const appData = await checkTestFlightStatus(testFlightID); currentStatus[testFlightID] = appData; // Check if status changed const wasStatusChange = previousStatus[testFlightID] && previousStatus[testFlightID].status !== appData.status; // Send notification for status changes, or initial run if (!previousStatus[testFlightID] || wasStatusChange) { await sendDiscordNotification(appData, wasStatusChange); if (wasStatusChange) { console.log(`Status changed for ${testFlightID}: ${previousStatus[testFlightID].status} โ†’ ${appData.status}`); } } await new Promise(resolve => setTimeout(resolve, 2000)); } await saveCurrentStatus(currentStatus); } catch (error) { console.error('[error] Error during monitoring:', error.message); } } // Start monitoring async function startMonitoring() { console.log('[info] TestFlight discord monitor started'); console.log('[info] Checking apps every 60 seconds...'); // Run initial check await monitorTestFlightApps(); // Set up interval to run every minute (60000ms) setInterval(async () => { console.log('\n[system] Running scheduled check'); await monitorTestFlightApps(); }, 60000); } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\n[info] Shutting down TestFlight monitor...'); process.exit(0); }); process.on('SIGTERM', () => { console.log('\n[info] Shutting down TestFlight monitor...'); process.exit(0); }); // Test webhook function async function testWebhook() { console.log('[info] Testing Discord webhook...'); const testPayload = { embeds: [{ title: '๐Ÿงช TestFlight Monitor Test', description: 'This is a test message to verify the webhook is working.', color: 0x00ff00, timestamp: new Date().toISOString(), footer: { text: 'TestFlight Monitor Test' } }] }; try { const response = await axios.post(DISCORD_WEBHOOK_URL, testPayload, { headers: { 'Content-Type': 'application/json' } }); console.log(`[system] Test webhook sent successfully - Response: ${response.status}`); } catch (error) { console.error(`[error] Test webhook failed:`, error.message); if (error.response) { console.error(`[error] Discord API Response:`, error.response.status, error.response.data); } } } // Start the monitoring if (require.main === module) { // Check if first argument is 'test' to run webhook test if (process.argv[2] === 'test') { testWebhook().catch(error => console.error('[error] Error testing webhook:', error.message)); } else { startMonitoring().catch(error => console.error('[error] Error starting monitoring:', error.message)); } } module.exports = { monitorTestFlightApps, checkTestFlightStatus, sendDiscordNotification, parseTestFlightID, buildTestFlightURL };