mirror of
https://github.com/MaximilianGT500/testflight-watcher.git
synced 2025-01-10 05:08: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