This commit is contained in:
Maximilian Stumpf 2024-12-14 00:55:54 +01:00
commit cfba34f012
11 changed files with 1172 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Maximilian Stumpf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
PushoverAPI.js Normal file
View File

@ -0,0 +1,47 @@
const axios = require("axios");
const HTTP_STATUS_OK = 200;
class PushoverAPI {
constructor(userKey, appToken) {
this.userKey = userKey;
this.appToken = appToken;
this.url = "https://api.pushover.net:443/1/messages.json";
}
async sendNotification(title, message, options = {}) {
const data = {
user: this.userKey,
token: this.appToken,
message: message,
title: title,
priority: 0,
...options,
};
try {
const response = await axios.post(this.url, new URLSearchParams(data));
if (response.status === HTTP_STATUS_OK) {
console.log("✅ Die Nachricht wurde erfolgreich gesendet!");
return response.data;
} else {
console.error(
`❌ Fehler: Unbekannter Fehler beim Senden der Nachricht. Statuscode: ${response.status}`
);
return null;
}
} catch (error) {
if (error.response) {
console.error(`❌ Fehler von Pushover: ${error.response.data.errors}`);
console.error(`🔧 Statuscode: ${error.response.status}`);
} else if (error.request) {
console.error("❌ Fehler bei der Anfrage: Keine Antwort erhalten.");
} else {
console.error("❌ Fehler: " + error.message);
}
return null;
}
}
}
module.exports = PushoverAPI;

103
README.md Normal file
View File

@ -0,0 +1,103 @@
# TestFlight Watcher 🚀
## Beschreibung 📜
Der **TestFlight Watcher** ist ein Skript, das TestFlight-Betas überwacht und Benachrichtigungen versendet, wenn neue Plätze für Betatests verfügbar sind. Es prüft regelmäßig die angegebenen TestFlight-URLs und benachrichtigt den Benutzer, wenn Plätze verfügbar werden. Diese Benachrichtigungen werden über die [Pushover-API](https://pushover.net/) versendet.
## Funktionen 🛠️
- Überwachung von TestFlight-Betas.
- Senden von Benachrichtigungen bei Verfügbarkeit von Testplätzen.
- Einfache Verwaltung der zu überwachenden URLs über eine Konfigurationsdatei oder über das Setup-Script.
- Webserver zum Löschen von bereits angenommenen Beta-Einladungen.
- Einstellbare Priorität
- Einstellbares Prüfinterval
## Installation 💻
### Voraussetzungen
- Internetverbindung
- 128MB Arbeitsspeicher
- 0,5 - 1 Thread
- [Node.js](https://nodejs.org/) (Version 12 oder höher)
- [Pushover Account](https://pushover.net/) (für Benachrichtigungen)
### Schritte
1. **Klone das Repository:**
```bash
git clone https://github.com/MaximilianGT500/testflight-watcher.git
cd testflight-watcher
```
2. **Installiere die benötigten Pakete:**
```bash
npm install
```
3. **Erstellung der `.env`-Datei:**
Falls noch nicht vorhanden, erstelle die `.env`-Datei, indem Du den Setup-Prozess ausführen:
```bash
node setup.js / npm run setup
```
4. **Starte das Skript:**
```bash
node index.js / npm start
```
## Konfiguration ⚙️
In der `.env`-Datei müssen folgende Variablen konfiguriert werden:
- **PUSHOVER_USER_KEY**: Dein Pushover-Benutzer-Schlüssel.
- **PUSHOVER_APP_TOKEN**: Dein Pushover-App-Token.
- **TESTFLIGHT_URLS**: Eine Liste der TestFlight-URLs, die überwacht werden sollen. Beispiel:
```json
[{ "name": "App 1", "url": "https://testflight.apple.com/join/abcd1234" },{ "name": "App 2", "url": "https://testflight.apple.com/join/xyz5678" }]
```
- **OTP_SECRET**: Ein geheimer Schlüssel für die OTP-Generierung.
- **OTP_VALIDITY**: Die Gültigkeitsdauer des OTP in Sekunden (Standard: 300).
- **USER_AGENT**: Der User-Agent für Anfragen an die TestFlight-URLs.
- **PORT**: Der Port, auf dem der Webserver läuft (Standard: 3000).
- **HTTP_URL**: Die URL des Webservers (Standard: `http://localhost:3000`).
- **PUSHOVER_PRIORITY**: Die Priorität der Benachrichtigungen bei freier Beta. (Standard: 1)
- **CHECK_INTERVAL**: Wie oft soll nach ein freien Platz geprüft werden? (Standard: 30)
### Beispiel `.env`-Datei:
```env
PUSHOVER_USER_KEY=dein_pushover_benutzer_key
PUSHOVER_APP_TOKEN=dein_pushover_app_token
TESTFLIGHT_URLS='[{"name":"App 1", "url":"https://testflight.apple.com/join/abcd1234"}]'
OTP_SECRET=AendereDiesenString
OTP_VALIDITY=300
USER_AGENT=Testflight-Watcher/0.0.1
PORT=3000
HTTP_URL=http://localhost:3000
PUSHOVER_PRIORITY=1
CHECK_INTERVAL=30
```
## Nutzung 🚀
### Überwachung starten
Nachdem das Skript erfolgreich gestartet wurde, überwacht es kontinuierlich die angegebenen TestFlight-URLs und prüft standardmäßig alle 30 Sekunden auf ein neuen Platz.
### Benachrichtigungen
Wenn ein Platz für einen TestFlight-Betatests verfügbar ist, wird automatisch eine Benachrichtigung an den angegebenen Pushover-Benutzer gesendet.
### URLs verwalten
Du kannst TestFlight-URLs über die Konsole verwalten, indem du die `.env`-Datei bearbeitest oder `npm run setup` bzw. `node setup.js` ausführst.
## Setup 📦
Beim ersten Start des Skripts musst Du die Konfiguration einrichten. Falls die `.env`-Datei fehlt oder unvollständig ist, wird automatisch das Setup gestartet, um die fehlenden Werte zu konfigurieren.
## Debugging und Fehlerbehebung ⚠️
- **Fehlende `.env`-Datei**: Wenn die `.env`-Datei fehlt, startet das Skript den Setup-Prozess automatisch.
- **Pushover-Benachrichtigungen**: Überprüfe, ob der Benutzer-Schlüssel und das App-Token korrekt sind, wenn keine Benachrichtigungen gesendet werden.
- **TestFlight-URLs**: Stelle sicher, dass die URLs korrekt sind und auf existierende Betatests verweisen.
## Lizenz 📄
Dieses Projekt ist unter der MIT-Lizenz lizenziert - siehe die [LICENSE](LICENSE)-Datei für Details.
---

300
index.js Normal file
View File

@ -0,0 +1,300 @@
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
);
})();

5
nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": [".env"],
"ext": "js,json",
"exec": "node index.js"
}

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"dependencies": {
"axios": "^1.7.9",
"cheerio": "^1.0.0",
"cli-color": "^2.0.4",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"nodemon": "^3.1.7"
},
"name": "testflight-watcher",
"version": "0.0.1",
"description": "Dies ist ein einfacher TestFlight-Watcher, der alle 30 Sekunden die Website auf verfügbare Plätze überprüft. Sobald ein Platz frei wird, erfolgt eine Benachrichtigung über Pushover.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js",
"setup": "node setup.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/MaximilianGT500/testflight-watcher.git"
},
"keywords": [
"testflight",
"watcher",
"testflight-watcher",
"beta",
"beta-sniper",
"sniper"
],
"author": "MaximilianGT500",
"license": "MIT",
"bugs": {
"url": "https://github.com/MaximilianGT500/testflight-watcher/issues"
},
"homepage": "https://github.com/MaximilianGT500/testflight-watcher#readme"
}

116
public/assets/css/error.css Normal file
View File

@ -0,0 +1,116 @@
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 15px rgba(255, 111, 97, 0.5);
}
50% {
box-shadow: 0 0 25px rgba(255, 111, 97, 1);
}
}
body {
font-family: "Roboto", sans-serif;
background-color: #121212;
color: #e0e0e0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
animation: fadeIn 1s ease-in-out;
}
.error-container {
max-width: 600px;
padding: 50px;
background-color: #1e1e1e;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
border-radius: 12px;
animation: fadeIn 1.5s ease-in-out;
}
.error-title {
font-size: 2.5em;
color: #ff6f61;
margin-bottom: 15px;
animation: fadeIn 2s ease-in-out;
}
.error-message {
margin-bottom: 25px;
font-size: 1.2em;
animation: fadeIn 2.5s ease-in-out;
}
.crossmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #dc3545;
stroke-miterlimit: 10;
margin: 10% auto;
box-shadow: inset 0px 0px 0px #dc3545;
animation: fill 0.4s ease-in-out 0.4s forwards,
scale 0.3s ease-in-out 0.9s both;
}
.crossmark__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #dc3545;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.crossmark__cross {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
stroke: #fff;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.7s forwards;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes scale {
0%,
100% {
transform: none;
}
50% {
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes fill {
100% {
box-shadow: inset 0px 0px 0px 30px #dc3545;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -0,0 +1,57 @@
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 15px rgba(255, 111, 97, 0.5);
}
50% {
box-shadow: 0 0 25px rgba(255, 111, 97, 1);
}
}
body {
font-family: "Roboto", sans-serif;
background-color: #121212;
color: #e0e0e0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
animation: fadeIn 1s ease-in-out;
}
.container {
max-width: 600px;
padding: 50px;
background-color: #1e1e1e;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
border-radius: 12px;
animation: fadeIn 1.5s ease-in-out;
}
.title {
font-size: 2.5em;
color: #2a28a7;
margin-bottom: 15px;
animation: fadeIn 2s ease-in-out;
}
.message {
margin-bottom: 25px;
font-size: 1.2em;
animation: fadeIn 2.5s ease-in-out;
}

View File

@ -0,0 +1,137 @@
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 15px rgba(255, 111, 97, 0.5);
}
50% {
box-shadow: 0 0 25px rgba(255, 111, 97, 1);
}
}
body {
font-family: "Roboto", sans-serif;
background-color: #121212;
color: #e0e0e0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
animation: fadeIn 1s ease-in-out;
}
.success-container {
max-width: 600px;
padding: 50px;
background-color: #1e1e1e;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
border-radius: 12px;
animation: fadeIn 1.5s ease-in-out;
}
.success-title {
font-size: 2.5em;
color: #28a745;
margin-bottom: 15px;
animation: fadeIn 2s ease-in-out;
}
.success-message {
margin-bottom: 25px;
font-size: 1.2em;
animation: fadeIn 2.5s ease-in-out;
}
.crossmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #28a745;
stroke-miterlimit: 10;
margin: 10% auto;
box-shadow: inset 0px 0px 0px #dc3545;
animation: fill 0.4s ease-in-out 0.4s forwards,
scale 0.3s ease-in-out 0.9s both;
}
.checkmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #28a745;
stroke-miterlimit: 10;
margin: 10% auto;
box-shadow: inset 0px 0px 0px #28a745;
animation: fill 0.4s ease-in-out 0.4s forwards,
scale 0.3s ease-in-out 0.9s both;
}
.checkmark__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #28a745;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark__check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
stroke: #fff;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.7s forwards;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes scale {
0%,
100% {
transform: none;
}
50% {
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes fill {
100% {
box-shadow: inset 0px 0px 0px 30px #28a745;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

346
setup.js Normal file
View File

@ -0,0 +1,346 @@
const fs = require("fs");
const readline = require("readline");
const axios = require("axios");
const PushoverAPI = require("./PushoverAPI");
const clc = require("cli-color");
require("dotenv").config();
const ENV_FILE = ".env";
const TESTFLIGHT_BASE_URL = "https://testflight.apple.com/join/";
function loadEnv() {
if (fs.existsSync(ENV_FILE)) {
const content = fs.readFileSync(ENV_FILE, "utf8");
return Object.fromEntries(
content
.split("\n")
.filter((line) => line && !line.startsWith("#"))
.map((line) => line.split("=").map((part) => part.trim()))
);
}
return {};
}
function saveEnv(env) {
const content = Object.entries(env)
.map(([key, value]) => `${key}=${value}`)
.join("\n");
fs.writeFileSync(ENV_FILE, content);
}
function saveURLs(urls) {
const env = loadEnv();
env.TESTFLIGHT_URLS = JSON.stringify(urls);
saveEnv(env);
}
function loadURLs() {
const env = loadEnv();
return env.TESTFLIGHT_URLS ? JSON.parse(env.TESTFLIGHT_URLS) : [];
}
async function prompt(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
})
);
}
async function verifyURL(url) {
try {
const response = await axios.get(url, { timeout: 5000 });
if (response.status === 200) {
console.log(
clc.greenBright("\n✅ Die URL ist gültig und die Beta existiert.")
);
return true;
}
} catch (error) {
const errorMessage =
error.response && error.response.status === 404
? "\n❌ Fehler: Die Beta existiert nicht (404)."
: "❌ Fehler: Konnte die Seite nicht überprüfen. Netzwerkproblem?";
console.log(clc.redBright(errorMessage));
}
return false;
}
function displayMessage(message, type = "info") {
const color =
type === "success"
? clc.green
: type === "error"
? clc.red
: clc.magentaBright;
console.log(color(message));
}
async function manageURLs(userKey, appToken) {
let urls = loadURLs();
while (true) {
clearConsole();
displayMessage("📜 Aktuelle TestFlight-URLs:", "info");
urls.forEach((app, index) => {
console.log(clc.yellow(`${index + 1}. ${app.name} - ${app.url}`));
});
console.log("\n🛠 Optionen:");
console.log("1. 🆕 Neue URL hinzufügen");
console.log("2. 🗑️ Existierende URL löschen");
console.log("3. ✅ Fertig");
const choice = await prompt(clc.cyan("Wähle eine Option (1/2/3): "));
if (choice === "1") {
const name = await prompt(clc.cyan("\nApp-Name: "));
let input = await prompt(clc.cyan("TestFlight-URL oder ID: "));
let url = input.startsWith(TESTFLIGHT_BASE_URL)
? input
: `${TESTFLIGHT_BASE_URL}${input}`;
if (urls.some((app) => app.url === url)) {
displayMessage("\n❌ Diese URL wurde bereits hinzugefügt.", "error");
displayMessage("🔙 Rückkehr zum Hauptmenü...", "info");
await pause(3000);
continue;
}
const isValid = await verifyURL(url);
if (isValid) {
urls.push({ name, url });
saveURLs(urls);
await sendPushoverNotification(
userKey,
appToken,
"🆕 TestFlight-URL hinzugefügt",
`Die TestFlight-Beta für ${name} ist jetzt verfügbar.\n\nURL: ${url}`
);
displayMessage("\n📲 Benachrichtigung gesendet.", "success");
displayMessage("✅ Neue URL hinzugefügt.", "success");
displayMessage("🔙 Rückkehr zum Hauptmenü...", "info");
await pause(3000);
} else {
displayMessage(
"❌ Die URL wurde nicht hinzugefügt, da sie ungültig ist.",
"error"
);
displayMessage("🔙 Rückkehr zum Hauptmenü...", "info");
await pause(3000);
}
} else if (choice === "2") {
const index =
parseInt(await prompt(clc.cyan("Nummer der zu löschenden URL: ")), 10) -
1;
if (index >= 0 && index < urls.length) {
displayMessage(
`\n🗑️ Lösche: ${urls[index].name} - ${urls[index].url}`,
"error"
);
await sendPushoverNotification(
userKey,
appToken,
"🗑️ TestFlight-URL gelöscht",
`Die TestFlight-Beta für ${urls[index].name} wurde gelöscht.\n\nURL: ${urls[index].url}`
);
displayMessage("\n📲 Benachrichtigung gesendet.", "success");
urls.splice(index, 1);
saveURLs(urls);
displayMessage("✅ URL gelöscht.", "success");
displayMessage("🔙 Rückkehr zum Hauptmenü...", "info");
await pause(3000);
} else {
displayMessage("❌ Ungültige Auswahl.", "error");
await pause(3000);
}
} else if (choice === "3") {
break;
} else {
displayMessage("❌ Ungültige Eingabe.", "error");
await pause(3000);
}
}
}
async function sendPushoverNotification(userKey, appToken, title, message) {
const pushover = new PushoverAPI(userKey, appToken);
try {
const response = await pushover.sendNotification(title, message, {
priority: 0,
});
if (!response || response.status !== 1) {
throw err;
}
return response;
} catch (err) {
throw err;
}
}
async function configureEnvironment() {
const env = loadEnv();
displayMessage("\n🎉 Willkommen zum Setup!\n", "info");
const questions = [
{
key: "OTP_SECRET",
prompt:
'Verschlüsselungsstring für das OTP (z.B. "768XuxTKWXKUPQ8fjfLxCtUQCVEKikq6")',
defaultValue: "AendereDiesenString",
},
{
key: "OTP_VALIDITY",
prompt:
'Gültigkeitsdauer für das OTP in Sekunden (z.B. "300" für 5 Min.)',
defaultValue: "300",
},
{
key: "SETUP_FILE_NAME",
prompt: "Name der Setup-Datei (Falls Du ihn angepasst hast)",
defaultValue: "setup.js",
},
{
key: "SERVER_FILE_NAME",
prompt: "Name der Server-Datei (Fals Du ihn angepasst hast)",
defaultValue: "index.js",
},
{
key: "PORT",
prompt: 'Port für den Server (z.B. "3000")',
defaultValue: "3000",
},
{
key: "HTTP_URL",
prompt:
"Auf welche Adresse ist der Webserver erreichbar? (z.B. http://localhost:3000)",
defaultValue: "http://localhost:3000",
},
{
key: "USER_AGENT",
prompt: "User-Agent für Anfragen",
defaultValue: "Testflight-Watcher/0.0.1 (Monitoring Script)",
},
{
key: "PUSHOVER_PRIORITY",
prompt:
"Priorität der Benachrichtigung bei freier Beta (Niedrigste = -2; Niedrige = -1; Normale = 0; Hohe = 1; Kritische = 2)",
defaultValue: "1",
},
{
key: "CHECK_INTERVAL",
prompt:
"In welchen Abstand soll das Script nach einen neuen Platz prüfen in Sekunden (z.B. 30)",
defaultValue: "30",
},
];
for (const { key, prompt: question, defaultValue } of questions) {
if (env[key]) {
displayMessage(
`⏩ Überspringe: ${key} (bereits gesetzt: ${env[key]})`,
"info"
);
continue;
}
const currentValue = defaultValue;
const answer = await prompt(
clc.cyan(`${question} [Standard: ${currentValue}]: `)
);
env[key] = answer || currentValue;
saveEnv(env);
}
saveEnv(env);
displayMessage("\n📂 Standard-Konfiguration gespeichert.", "success");
}
(async () => {
const env = loadEnv();
await configureEnvironment();
if (!env.PUSHOVER_USER_KEY || !env.PUSHOVER_APP_TOKEN) {
env.PUSHOVER_USER_KEY = await prompt(
clc.cyan("\nPushover-Benutzer-Schlüssel: ")
);
env.PUSHOVER_APP_TOKEN = await prompt(clc.cyan("Pushover-API-Token: "));
let testApiSuccess = false;
while (!testApiSuccess) {
const testApi = await prompt(
clc.yellow("Möchtest Du die Pushover-Verbindung testen? (") +
clc.green("ja") +
clc.yellow("/") +
clc.red("nein") +
clc.yellow("): ")
);
if (testApi.toLowerCase() === "ja") {
try {
await sendPushoverNotification(
env.PUSHOVER_USER_KEY,
env.PUSHOVER_APP_TOKEN,
"Pushover-Verbindung",
"Die Pushover-API ist erfolgreich konfiguriert!"
);
testApiSuccess = true;
displayMessage(
"✅ Die Verbindung zu Pushover war erfolgreich!",
"success"
);
saveEnv(env);
displayMessage(
"\n\n📂 Pushover-Konfiguration gespeichert.",
"success"
);
await pause(3000);
} catch (error) {
const retry = await prompt(
clc.yellow("\nMöchtest Du die Daten korrigieren? (") +
clc.green("ja") +
clc.yellow("/") +
clc.red("nein") +
clc.yellow("): ")
);
if (retry.toLowerCase() === "ja") {
env.PUSHOVER_USER_KEY = await prompt(
clc.cyan("\nPushover-Benutzer-Schlüssel: ")
);
env.PUSHOVER_APP_TOKEN = await prompt(
clc.cyan("Pushover-API-Token: ")
);
saveEnv(env);
}
}
} else {
displayMessage("\nVerbindungstest übersprungen.", "info");
saveEnv(env);
displayMessage("📂 Pushover-Konfiguration gespeichert.", "success");
await pause(3000);
testApiSuccess = true;
}
}
} else {
displayMessage("✅ Pushover ist bereits konfiguriert.", "success");
await pause(3000);
}
displayMessage("\n🛠 Verwalte TestFlight-URLs:", "info");
await manageURLs(env.PUSHOVER_USER_KEY, env.PUSHOVER_APP_TOKEN);
displayMessage("\n✅ Setup abgeschlossen!", "success");
})();
async function pause(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function clearConsole() {
process.stdout.write("\x1Bc");
}

3
version.json Normal file
View File

@ -0,0 +1,3 @@
{
"version": "0.0.1"
}