commit cfba34f0129c99dfd754287cd487f873789efcb5 Author: MaximilianGT500 Date: Sat Dec 14 00:55:54 2024 +0100 0.0.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0849c15 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/PushoverAPI.js b/PushoverAPI.js new file mode 100644 index 0000000..cd36e2f --- /dev/null +++ b/PushoverAPI.js @@ -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; diff --git a/README.md b/README.md new file mode 100644 index 0000000..e82434e --- /dev/null +++ b/README.md @@ -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. + +--- \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..06b5b97 --- /dev/null +++ b/index.js @@ -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(` + + + + + Testflight Watcher | Startseite + + + +
+
Testfligt-Watcher
+
Diese Anwendung überwacht TestFlight-URLs und sendet Benachrichtigungen, wenn Plätze verfügbar sind.
+
+ +`); + }); + + app.get("/delete", (req, res) => { + const otp = req.query.otp; + const urlToDelete = req.query.url; + + if (!verifyOTP(otp, urlToDelete)) { + return res.status(403).send( + ` + + + + + Testflight Watcher | Fehler - Ungültiger Token + + + +
+ + + + +
Ungültig
+
Das OTP ist ungültig.
Bitte überprüfe Deine Eingaben.
+
+ +` + ); + } + + TESTFLIGHT_URLS = TESTFLIGHT_URLS.filter((app) => app.url !== urlToDelete); + + updateTestFlightURLs(TESTFLIGHT_URLS); + + res.send( + ` + + + + + + Testflight Watcher | Erfolgreich - ${urlToDelete} gelöscht + + + + +
+ + + + +
Erfolgreich
+
TestFlight-URL für ${urlToDelete} wurde erfolgreich gelöscht
+
+ + +` + ); + 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 + ); +})(); diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..b8b65c1 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": [".env"], + "ext": "js,json", + "exec": "node index.js" + } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..59f00e0 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/public/assets/css/error.css b/public/assets/css/error.css new file mode 100644 index 0000000..fedd590 --- /dev/null +++ b/public/assets/css/error.css @@ -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; + } +} diff --git a/public/assets/css/homepage.css b/public/assets/css/homepage.css new file mode 100644 index 0000000..d6acd47 --- /dev/null +++ b/public/assets/css/homepage.css @@ -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; +} \ No newline at end of file diff --git a/public/assets/css/success.css b/public/assets/css/success.css new file mode 100644 index 0000000..6ffef36 --- /dev/null +++ b/public/assets/css/success.css @@ -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; + } +} diff --git a/setup.js b/setup.js new file mode 100644 index 0000000..4b95cb0 --- /dev/null +++ b/setup.js @@ -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"); +} diff --git a/version.json b/version.json new file mode 100644 index 0000000..a8908f6 --- /dev/null +++ b/version.json @@ -0,0 +1,3 @@ +{ + "version": "0.0.1" +} \ No newline at end of file