mirror of
https://github.com/MaximilianGT500/testflight-watcher.git
synced 2025-01-10 10:28:34 +01:00
0.0.1
This commit is contained in:
commit
cfba34f012
21
LICENSE
Normal file
21
LICENSE
Normal 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
47
PushoverAPI.js
Normal 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
103
README.md
Normal 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
300
index.js
Normal 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
5
nodemon.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": [".env"],
|
||||||
|
"ext": "js,json",
|
||||||
|
"exec": "node index.js"
|
||||||
|
}
|
37
package.json
Normal file
37
package.json
Normal 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
116
public/assets/css/error.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
57
public/assets/css/homepage.css
Normal file
57
public/assets/css/homepage.css
Normal 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;
|
||||||
|
}
|
137
public/assets/css/success.css
Normal file
137
public/assets/css/success.css
Normal 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
346
setup.js
Normal 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
3
version.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user