2024-12-14 00:55:54 +01:00
const fs = require ( "fs" ) ;
const { execSync } = require ( "child_process" ) ;
require ( "dotenv" ) . config ( ) ;
const axios = require ( "axios" ) ;
const PushoverAPI = require ( "./PushoverAPI" ) ;
const cheerio = require ( "cheerio" ) ;
const clc = require ( "cli-color" ) ;
const crypto = require ( "crypto" ) ;
const path = require ( "path" ) ;
const express = require ( "express" ) ;
2024-12-20 21:58:43 +01:00
const CURRENT _VERSION = "0.0.2" ;
2024-12-14 00:55:54 +01:00
const UPDATE _CHECK _URL =
2024-12-15 00:55:23 +01:00
"https://git.lunoxia.net/MaximilianGT500/testflight-watcher/raw/branch/main/version.json" ;
2024-12-14 00:55:54 +01:00
const SETUP _FILE _NAME = process . env . SETUP _FILE _NAME || "setup.js" ;
const SERVER _FILE _NAME = process . env . SERVER _FILE _NAME || "index.js" ;
const USER _AGENT =
2024-12-20 21:58:43 +01:00
process . env . USER _AGENT || "Testflight-Watcher/0.0.2 (Monitoring Script)" ;
2024-12-14 00:55:54 +01:00
const OTP _SECRET = process . env . OTP _SECRET || "AendereDiesenString" ;
2024-12-20 21:58:43 +01:00
const OTP _VALIDITY = process . env . OTP _VALIDITY || "5 * 60 * 1000" ;
2024-12-14 00:55:54 +01:00
const PORT = process . env . PORT || "3000" ;
const HTTP _URL = process . env . HTTP _URL || ` http://localhost: ${ PORT } ` ;
const PUSHOVER _PRIORITY = process . env . PUSHOVER _PRIORITY || ` 1 ` ;
const CHECK _INTERVAL = process . env . CHECK _INTERVAL || ` 30 ` ;
let TESTFLIGHT _URLS = [ ] ;
2024-12-20 21:58:43 +01:00
let OTP _STORAGE = { } ;
2024-12-14 00:55:54 +01:00
function loadConfig ( ) {
if ( ! fs . existsSync ( ".env" ) ) {
console . log (
clc . yellowBright ( "⚠️ Konfigurationsdatei `.env` fehlt. Starte Setup..." )
) ;
execSync ( ` node ${ SETUP _FILE _NAME } ` , { stdio : "inherit" } ) ;
require ( "dotenv" ) . config ( ) ;
}
const PUSHOVER _USER _KEY = process . env . PUSHOVER _USER _KEY ;
const PUSHOVER _APP _TOKEN = process . env . PUSHOVER _APP _TOKEN ;
TESTFLIGHT _URLS = process . env . TESTFLIGHT _URLS
? JSON . parse ( process . env . TESTFLIGHT _URLS )
: [ ] ;
if ( ! PUSHOVER _USER _KEY || ! PUSHOVER _APP _TOKEN ) {
console . error (
clc . redBright (
` ❌ Fehlende Konfiguration. Bitte führe das Setup erneut aus: "node ${ SETUP _FILE _NAME } ". `
)
) ;
execSync ( ` node ${ SETUP _FILE _NAME } ` , { stdio : "inherit" } ) ;
require ( "dotenv" ) . config ( ) ;
process . exit ( 1 ) ;
}
if ( ! Array . isArray ( TESTFLIGHT _URLS ) || TESTFLIGHT _URLS . length === 0 ) {
console . error (
clc . redBright (
"❌ Keine gültigen TestFlight-URLs in der Konfiguration gefunden. Bitte führe das Setup erneut aus."
)
) ;
execSync ( ` node ${ SETUP _FILE _NAME } ` , { stdio : "inherit" } ) ;
process . exit ( 1 ) ;
}
return { PUSHOVER _USER _KEY , PUSHOVER _APP _TOKEN } ;
}
async function checkForUpdates ( ) {
try {
console . clear ( ) ;
console . log ( clc . cyan ( "🔄 Prüfe auf Updates..." ) ) ;
const { data } = await axios . get ( UPDATE _CHECK _URL ) ;
const latestVersion = data . version ;
if ( latestVersion !== CURRENT _VERSION ) {
console . log (
clc . yellowBright (
` 🚨 Neue Version verfügbar: ${ latestVersion } . Bitte aktualisiere das Skript. \n `
)
) ;
} else {
console . log ( clc . green ( "✅ Skript ist auf dem neuesten Stand.\n" ) ) ;
}
} catch ( error ) {
console . error (
clc . redBright ( "❌ Fehler beim Prüfen auf Updates:" , error . message )
) ;
}
}
function updateTestFlightURLs ( newURLs ) {
const envPath = path . resolve ( _ _dirname , ".env" ) ;
let envFile = fs . readFileSync ( envPath , "utf8" ) ;
const testflightUrlsRegex = /TESTFLIGHT_URLS=[^\n]*/ ;
const newTestFlightUrls = ` TESTFLIGHT_URLS= ${ JSON . stringify ( newURLs ) } ` ;
if ( testflightUrlsRegex . test ( envFile ) ) {
envFile = envFile . replace ( testflightUrlsRegex , newTestFlightUrls ) ;
} else {
envFile += ` \n ${ newTestFlightUrls } ` ;
}
fs . writeFileSync ( envPath , envFile , "utf8" ) ;
console . log (
clc . green (
"✅ .env-Datei erfolgreich mit neuen TESTFLIGHT_URLS aktualisiert."
)
) ;
}
async function checkAllTestFlights ( TESTFLIGHT _URLS , pushoverAPI ) {
console . clear ( ) ;
const now = new Date ( ) . toLocaleTimeString ( ) ;
console . log ( clc . blue ( ` ⏰ Letzte Prüfung: ${ now } \n ` ) ) ;
const results = await Promise . all (
TESTFLIGHT _URLS . map ( ( app ) => checkTestFlight ( app , pushoverAPI ) )
) ;
results . forEach ( ( result ) => console . log ( result ) ) ;
console . log (
clc . blue (
` \n ✔️ Letzte Prüfung abgeschlossen: ${ now } ` +
clc . red ` \n \n Drücke STRG+C zum Beenden. `
)
) ;
}
async function checkTestFlight ( app , pushoverAPI ) {
try {
const { data } = await axios . get ( app . url , {
headers : {
Accept : "text/html" ,
"User-Agent" : USER _AGENT ,
} ,
} ) ;
const $ = cheerio . load ( data ) ;
const betaStatus = $ ( "div.beta-status span" ) . text ( ) . trim ( ) ;
if ( betaStatus === "This beta is full." ) {
return clc . red ( ` ❌ ${ app . name } : Beta-Test ist voll. ` ) ;
2024-12-15 00:55:23 +01:00
} else if (
betaStatus === "This beta isn't accepting any new testers right now."
) {
return clc . red ( ` ❌ ${ app . name } : Beta akzeptiert keine neuen Tester. ` ) ;
2024-12-14 00:55:54 +01:00
} else {
const otp = generateOTP ( app . url ) ;
2024-12-20 21:58:43 +01:00
OTP _STORAGE [ app . url ] = otp ;
2024-12-14 00:55:54 +01:00
await sendPushoverNotification (
pushoverAPI ,
` 🎉 TestFlight Beta verfügbar für ${ app . name } ! ` ,
` Der Beta-Test für ${ app . name } ist verfügbar. Jetzt anmelden! 🚀 \n \n ${ app . url } \n \n Rufe diesen Link auf, um die TestFlight-Beta-URL zu löschen: ${ HTTP _URL } /delete?otp= ${ otp } &url= ${ app . url } `
) ;
return clc . green (
` ✅ ${ app . name } : Beta-Test ist verfügbar! Benachrichtigung gesendet. (Status: ${ betaStatus } ) `
) ;
}
} catch ( error ) {
return clc . redBright (
` ❌ Fehler beim Abrufen der Seite für ${ app . name } : ${ error . message } `
) ;
}
}
async function sendPushoverNotification ( pushoverAPI , title , message ) {
const options = {
priority : PUSHOVER _PRIORITY ,
sound : "pushover" ,
} ;
const response = await pushoverAPI . sendNotification ( title , message , options ) ;
if ( ! response ) {
console . error ( clc . redBright ( "❌ Fehler beim Senden der Benachrichtigung." ) ) ;
}
}
function generateOTP ( url ) {
2024-12-20 21:58:43 +01:00
const timeWindow = Math . floor ( Date . now ( ) / OTP _VALIDITY ) ;
2024-12-14 00:55:54 +01:00
return crypto
. createHmac ( "sha256" , OTP _SECRET )
. update ( ` ${ timeWindow } - ${ url } ` )
. digest ( "hex" )
. slice ( 0 , 6 ) ;
}
function verifyOTP ( otp , url ) {
2024-12-20 21:58:43 +01:00
const timeWindow = Math . floor ( Date . now ( ) / OTP _VALIDITY ) ;
2024-12-14 00:55:54 +01:00
const validOTP = crypto
. createHmac ( "sha256" , OTP _SECRET )
. update ( ` ${ timeWindow } - ${ url } ` )
. digest ( "hex" )
. slice ( 0 , 6 ) ;
2024-12-20 21:58:43 +01:00
if ( otp === validOTP && OTP _STORAGE [ url ] === otp ) {
delete OTP _STORAGE [ url ] ;
2024-12-14 00:55:54 +01:00
return true ;
}
return false ;
}
function startServer ( TESTFLIGHT _URLS ) {
const app = express ( ) ;
app . use ( express . static ( "public" ) ) ;
app . get ( "/" , ( req , res ) => {
res . send ( ` <!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Testflight Watcher | Startseite < / t i t l e >
< link rel = "stylesheet" type = "text/css" href = "/assets/css/homepage.css" media = "all" >
< / h e a d >
< body >
< div class = "container" >
< div class = "title" > Testfligt - Watcher < / d i v >
< div class = "message" > Diese Anwendung überwacht TestFlight - URLs und sendet Benachrichtigungen , wenn Plätze verfügbar sind . < / d i v >
< / d i v >
< / b o d y >
< / h t m l > ` ) ;
} ) ;
app . get ( "/delete" , ( req , res ) => {
const otp = req . query . otp ;
const urlToDelete = req . query . url ;
if ( ! verifyOTP ( otp , urlToDelete ) ) {
return res . status ( 403 ) . send (
` <!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Testflight Watcher | Fehler - Ungültiger Token < / t i t l e >
< link rel = "stylesheet" type = "text/css" href = "/assets/css/error.css" media = "all" >
< / h e a d >
< body >
< div class = "error-container" >
< svg class = "crossmark" xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 52 52" >
< circle class = "crossmark__circle" cx = "26" cy = "26" r = "25" fill = "none" / >
< path class = "crossmark__cross" fill = "none" d = "M16 16l20 20M36 16l-20 20" / >
< / s v g >
< div class = "error-title" > Ungültig < / d i v >
< div class = "error-message" > Das OTP ist < b > ungültig < / b > . < / b r > B i t t e ü b e r p r ü f e D e i n e E i n g a b e n . < / d i v >
< / d i v >
< / b o d y >
< / h t m l > `
) ;
}
TESTFLIGHT _URLS = TESTFLIGHT _URLS . filter ( ( app ) => app . url !== urlToDelete ) ;
updateTestFlightURLs ( TESTFLIGHT _URLS ) ;
res . send (
` <!DOCTYPE html>
< html lang = "de" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Testflight Watcher | Erfolgreich - $ { urlToDelete } gelöscht < / t i t l e >
< link rel = "stylesheet" type = "text/css" href = "/assets/css/success.css" media = "all" >
< / h e a d >
< body >
< div class = "success-container" >
< svg class = "checkmark" xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 52 52" >
< circle class = "checkmark__circle" cx = "26" cy = "26" r = "25" fill = "none" / >
< path class = "checkmark__check" fill = "none" d = "M14.1 27.2l7.1 7.2 16.7-16.8" / >
< / s v g >
< div class = "success-title" > Erfolgreich < / d i v >
< div class = "success-message" > TestFlight - URL für $ { urlToDelete } wurde < b > erfolgreich < / b > g e l ö s c h t < / d i v >
< / d i v >
< / b o d y >
< / h t m l > `
) ;
execSync ( ` pm2 restart ${ SERVER _FILE _NAME } ` ) ;
} ) ;
app . listen ( PORT , ( ) => {
console . log (
clc . green ( "🔧 Express-Server läuft." ) +
clc . red ( "\n\nDrücke STRG+C zum Beenden." )
) ;
} ) ;
}
( async ( ) => {
const { PUSHOVER _USER _KEY , PUSHOVER _APP _TOKEN } = loadConfig ( ) ;
const pushoverAPI = new PushoverAPI ( PUSHOVER _USER _KEY , PUSHOVER _APP _TOKEN ) ;
await checkForUpdates ( ) ;
console . log ( clc . green ( "🔧 TestFlight-Monitor gestartet." ) ) ;
startServer ( TESTFLIGHT _URLS ) ;
setInterval (
( ) => checkAllTestFlights ( TESTFLIGHT _URLS , pushoverAPI ) ,
CHECK _INTERVAL * 1000
) ;
} ) ( ) ;