mirror of
https://github.com/MaximilianGT500/testflight-watcher.git
synced 2025-01-10 18:08:34 +01:00
301 lines
9.2 KiB
JavaScript
301 lines
9.2 KiB
JavaScript
|
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");
|
||
|
|
||
|
const CURRENT_VERSION = "0.0.1";
|
||
|
const UPDATE_CHECK_URL =
|
||
|
"https://raw.githubusercontent.com/MaximilianGT500/testflight-watcher/refs/heads/main/version.json";
|
||
|
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 =
|
||
|
process.env.USER_AGENT || "Testflight-Watcher/0.0.1 (Monitoring Script)";
|
||
|
const OTP_SECRET = process.env.OTP_SECRET || "AendereDiesenString";
|
||
|
const OTP_VALIDITY = process.env.OTP_VALIDITY || "300";
|
||
|
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 = [];
|
||
|
let OTP_STOREAGE = {};
|
||
|
|
||
|
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\nDrü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.`);
|
||
|
} else {
|
||
|
const otp = generateOTP(app.url);
|
||
|
OTP_STOREAGE[app.url] = otp;
|
||
|
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\nRufe 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) {
|
||
|
const timeWindow = Math.floor((Date.now() / OTP_VALIDITY) * 1000);
|
||
|
return crypto
|
||
|
.createHmac("sha256", OTP_SECRET)
|
||
|
.update(`${timeWindow}-${url}`)
|
||
|
.digest("hex")
|
||
|
.slice(0, 6);
|
||
|
}
|
||
|
|
||
|
function verifyOTP(otp, url) {
|
||
|
const timeWindow = Math.floor((Date.now() / OTP_VALIDITY) * 1000);
|
||
|
const validOTP = crypto
|
||
|
.createHmac("sha256", OTP_SECRET)
|
||
|
.update(`${timeWindow}-${url}`)
|
||
|
.digest("hex")
|
||
|
.slice(0, 6);
|
||
|
|
||
|
if (otp === validOTP && OTP_STOREAGE[url] === otp) {
|
||
|
delete OTP_STOREAGE[url];
|
||
|
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</title>
|
||
|
<link rel="stylesheet" type="text/css" href="/assets/css/homepage.css" media="all">
|
||
|
</head>
|
||
|
<body>
|
||
|
<div class="container">
|
||
|
<div class="title">Testfligt-Watcher</div>
|
||
|
<div class="message">Diese Anwendung überwacht TestFlight-URLs und sendet Benachrichtigungen, wenn Plätze verfügbar sind.</div>
|
||
|
</div>
|
||
|
</body>
|
||
|
</html>`);
|
||
|
});
|
||
|
|
||
|
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</title>
|
||
|
<link rel="stylesheet" type="text/css" href="/assets/css/error.css" media="all">
|
||
|
</head>
|
||
|
<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" />
|
||
|
</svg>
|
||
|
<div class="error-title">Ungültig</div>
|
||
|
<div class="error-message">Das OTP ist <b>ungültig</b>.</br>Bitte überprüfe Deine Eingaben.</div>
|
||
|
</div>
|
||
|
</body>
|
||
|
</html>`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
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</title>
|
||
|
<link rel="stylesheet" type="text/css" href="/assets/css/success.css" media="all">
|
||
|
</head>
|
||
|
|
||
|
<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" />
|
||
|
</svg>
|
||
|
<div class="success-title">Erfolgreich</div>
|
||
|
<div class="success-message">TestFlight-URL für ${urlToDelete} wurde <b>erfolgreich</b> gelöscht</div>
|
||
|
</div>
|
||
|
</body>
|
||
|
|
||
|
</html>`
|
||
|
);
|
||
|
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
|
||
|
);
|
||
|
})();
|