Created
August 14, 2025 16:27
-
-
Save veygax/7ca1c9b493f749879a1f252144935a3e to your computer and use it in GitHub Desktop.
discord testflight watcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
vibe coding