Skip to content

Instantly share code, notes, and snippets.

@veygax
Created August 14, 2025 16:27
Show Gist options
  • Select an option

  • Save veygax/7ca1c9b493f749879a1f252144935a3e to your computer and use it in GitHub Desktop.

Select an option

Save veygax/7ca1c9b493f749879a1f252144935a3e to your computer and use it in GitHub Desktop.
discord testflight watcher
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
};
@veygax
Copy link
Author

veygax commented Oct 24, 2025

vibe coding

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment