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
Copy Markdown
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