aktueller Stand
This commit is contained in:
6
.env
6
.env
@@ -1,9 +1,5 @@
|
||||
# MQTT-Konfiguration
|
||||
MQTT_BROKER=mqtt://iobroker:1884
|
||||
MQTT_TOPIC=foodsharing/pickupCheck/config/meik
|
||||
MQTT_USER=mqtt
|
||||
MQTT_PASSWORD=mqtt!1884!
|
||||
|
||||
ADMIN_EMAIL=meikdre@gmx.de
|
||||
# Server-Konfiguration
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
170
config/839246-pickup-config.json
Normal file
170
config/839246-pickup-config.json
Normal file
@@ -0,0 +1,170 @@
|
||||
[
|
||||
{
|
||||
"id": "44972",
|
||||
"label": "Aldi Süd RA Biblisweg",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": true
|
||||
},
|
||||
{
|
||||
"id": "44975",
|
||||
"label": "Aldi Süd RA Kuppenheim",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "44971",
|
||||
"label": "Aldi Süd RA LützowerStr.",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "63367",
|
||||
"label": "Arena Balkan Bäckerei",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "59378",
|
||||
"label": "Backwaren Kaufland",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "49712",
|
||||
"label": "Baden-Baden foodsharing",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "42264",
|
||||
"label": "Bildungshaus St. Bernhard, Rastatt",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "43191",
|
||||
"label": "Café Böckeler Baden-Baden",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "33875",
|
||||
"label": "CAP-Markt Sandweier",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "28513",
|
||||
"label": "denn's Biomarkt",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "42322",
|
||||
"label": "Edeka Fischer Haueneberstein",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "40082",
|
||||
"label": "Ernteaktionen rund um Baden-Baden",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "40261",
|
||||
"label": "Eventbetrieb Rastatt/Baden-Baden",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "51450",
|
||||
"label": "Hornbach KA-Grünwinkel",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "62551",
|
||||
"label": "Koordinationsbetrieb Fairteiler Sandweier ",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "53552",
|
||||
"label": "Koordinationsbetrieb Fairteiler Spitalkirche",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "43754",
|
||||
"label": "Koordinationsbetrieb Lebensmittelspenden für Geflüchtete bei Sinzheim",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "66125",
|
||||
"label": "Murgtal Foodsharing",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "62533",
|
||||
"label": "New Pop-Festival",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "31602",
|
||||
"label": "Notfallteam Rastatt/Baden-Baden / spontane Abholungen",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "63448",
|
||||
"label": "Penny Baden-Oos",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "41393",
|
||||
"label": "Weihnachtsmarkt Baden-Baden",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "22787",
|
||||
"label": "Weihnachtsmarkt Rastatt",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
},
|
||||
{
|
||||
"id": "36100",
|
||||
"label": "Bäckerei Späth",
|
||||
"active": false,
|
||||
"checkProfileId": true,
|
||||
"onlyNotify": false
|
||||
}
|
||||
]
|
||||
13
config/admin-settings.json
Normal file
13
config/admin-settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"scheduleCron": "*/10 7-22 * * *",
|
||||
"randomDelayMinSeconds": 10,
|
||||
"randomDelayMaxSeconds": 120,
|
||||
"initialDelayMinSeconds": 5,
|
||||
"initialDelayMaxSeconds": 30,
|
||||
"ignoredSlots": [
|
||||
{
|
||||
"storeId": "51450",
|
||||
"description": "TVS"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
config/credentials.json
Normal file
7
config/credentials.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"839246": {
|
||||
"email": "meikdre@gmx.de",
|
||||
"password": "R67aJUj2-wWVfP8",
|
||||
"token": "1fdccfbe-2182-4749-9f42-ac79345c143d"
|
||||
}
|
||||
}
|
||||
1
data/defaultConfig.js
Normal file
1
data/defaultConfig.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = [];
|
||||
@@ -1,390 +0,0 @@
|
||||
const weekdayMap={Montag:"Monday",Dienstag:"Tuesday",Mittwoch:"Wednesday",Donnerstag:"Thursday",Freitag:"Friday",Samstag:"Saturday",Sonntag:"Sunday"};
|
||||
|
||||
const configState = 'mqtt.0.foodsharing.pickupCheck.config.meik';
|
||||
let scheduledJobs = [];
|
||||
|
||||
// Zeitpl<70>ne aufr<66>umen
|
||||
function clearAllScheduledJobs() {
|
||||
scheduledJobs.forEach(job => clearSchedule(job));
|
||||
scheduledJobs = [];
|
||||
}
|
||||
|
||||
// Neue Funktion: Config aus dem Datenpunkt parsen
|
||||
function parseConfigState() {
|
||||
try {
|
||||
const raw = getState(configState)?.val;
|
||||
if (!raw) throw new Error("Leerer Konfigurationswert");
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) throw new Error("Konfiguration ist kein Array");
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Parsen der Konfiguration:", e.message);
|
||||
sendTelegramMessage(telegramInstance, `? Fehler beim Parsen der Pickup-Konfiguration: ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Pickup-Checks ausf<73>hren mit aktueller Konfiguration
|
||||
function setupPickupCheckSchedules() {
|
||||
clearAllScheduledJobs(); // Vorherige Tasks stoppen
|
||||
const pickupCheckConfig = parseConfigState();
|
||||
|
||||
pickupCheckConfig.forEach(({ id, active, onlyNotify, label, desiredWeekday, desiredDate, checkProfileId }) => {
|
||||
if (active) {
|
||||
const storeLabel = label || masterStores[id] || "Unbekannter Store";
|
||||
const translatedWeekday = desiredWeekday && weekdayMap[desiredWeekday] ? weekdayMap[desiredWeekday] : desiredWeekday;
|
||||
|
||||
const job = scheduleWithRandomDelay('*/10 7-22 * * *', () =>
|
||||
handleLoginAndRequests(
|
||||
(storeIds, session) => {
|
||||
console.log(`Pr<EFBFBD>fung f<>r ${storeLabel} (${id}) <20> Nur Benachrichtigung: ${onlyNotify}` +
|
||||
(translatedWeekday ? ` <20> nur an ${translatedWeekday}` : '') +
|
||||
` <20> checkProfileId: ${checkProfileId}`);
|
||||
checkPickupData(id, !!checkProfileId, session, onlyNotify, translatedWeekday, desiredDate);
|
||||
},
|
||||
null
|
||||
)
|
||||
);
|
||||
scheduledJobs.push(job); // Job merken
|
||||
}
|
||||
});
|
||||
}
|
||||
// Zentrale Definition der Stores mit ID und sprechendem Namen
|
||||
const masterStores = {
|
||||
"51485": "Hornbach KA - Hagsfeld",
|
||||
"43694": "Blumen Deniz Rastatt",
|
||||
"32807": "Claus Reformwaren",
|
||||
"47510": "BIOLAND G<>RTNEREI LUTZ",
|
||||
"42141": "Restaurant Pok<6F> You",
|
||||
"37245": "Erdbeerland Enderle GBR",
|
||||
"35401": "Kalinka - Internationale Lebensmittel",
|
||||
"31080": "Gem<65>sebau Gabelmann",
|
||||
"28513": "denn's Biomarkt",
|
||||
// Weitere Stores, die nur in Pickup-Checks genutzt werden:
|
||||
"63448": "Penny Baden-Oos",
|
||||
"42264": "Bildungshaus St. Bernhard, Rastatt",
|
||||
"51450": "Hornbach KA - Gr<47>nwinkel", // F<>r diese StoreID gilt die TVS-Ausnahme
|
||||
"42322": "Edeka Fischer Haueneberstein",
|
||||
"33875": "Cap Markt",
|
||||
"44972": "Aldi Biblisweg",
|
||||
"44975": "Aldi Kuppenheim"
|
||||
|
||||
};
|
||||
|
||||
// Extrahierte Store-IDs als Array f<>r sendFoodSharingRequests
|
||||
const storeIds = [
|
||||
//"51485", //Hornbach KA -Hagsfeld
|
||||
//"51450", //Hornbach KA-Gr<47>nwinkel
|
||||
"43694", //Blumen Deniz Rastatt
|
||||
"32807", //Claus Reformwaren
|
||||
"47510", //BIOLAND G<>RTNEREI LUTZ
|
||||
"42141", //Restaurant Pok<6F> You
|
||||
"37245", //Erdbeerland Enderle GBR
|
||||
"35401", //Kalinka - Internationale Lebensmittel
|
||||
"31080", //Gem<65>sebau Gabelmann
|
||||
"28513", //denn's Biomarkt
|
||||
//"29374", //Rastatter Wochenmarkt
|
||||
];
|
||||
|
||||
// Anmeldedaten
|
||||
const userEmail = "ddda@gmx.de";
|
||||
const userPassword = "a";
|
||||
|
||||
// Telegram-Bot-Instanz und Profil-ID (f<>r Pickup-Pr<50>fung)
|
||||
const telegramInstance = 'telegram.0';
|
||||
const profileId = 839246;
|
||||
|
||||
// Globale Variablen f<>r Session und CSRF-Token
|
||||
let sessionCookies = null;
|
||||
let csrfToken = null;
|
||||
|
||||
// Funktion zum Extrahieren des CSRF-Tokens aus den Cookies
|
||||
function extractCsrfToken(cookies) {
|
||||
let token = null;
|
||||
cookies.forEach(cookieStr => {
|
||||
if (cookieStr.includes("CSRF_TOKEN=")) {
|
||||
// Beispiel-Cookie: "CSRF_TOKEN=wert; Path=/; Domain=foodsharing.de; ..."
|
||||
token = cookieStr.split(';')[0].split('=')[1];
|
||||
}
|
||||
});
|
||||
return token;
|
||||
}
|
||||
|
||||
// Funktion zum zuf<75>llig verz<72>gerten Start des Skripts
|
||||
function startScriptWithRandomDelay(random = false) {
|
||||
const maxDelay = random ? 10 * 60 * 1000 : 1000; // bis zu 10 Minuten Verz<72>gerung
|
||||
const delay = Math.floor(Math.random() * maxDelay);
|
||||
console.log(`Das Skript wird in ${Math.round(delay / 1000)} Sekunden gestartet.`);
|
||||
setTimeout(() => {
|
||||
// sendFoodSharingRequests nutzt hier die zentrale masterStores-Liste
|
||||
handleLoginAndRequests(sendFoodSharingRequests, storeIds);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Session-Handling: Login und anschlie<69>ende Requests ausf<73>hren
|
||||
function handleLoginAndRequests(callback, storeIds = null) {
|
||||
checkSessionValidity().then(isValid => {
|
||||
if (!isValid) {
|
||||
login(userEmail, userPassword).then(() => {
|
||||
callback(storeIds, sessionCookies);
|
||||
}).catch(error => {
|
||||
console.error(`Login-Fehler: ${error}`);
|
||||
sendTelegramMessage(telegramInstance, "Login fehlgeschlagen.");
|
||||
});
|
||||
} else {
|
||||
callback(storeIds, sessionCookies);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// <20>berpr<70>fung der Session-G<>ltigkeit
|
||||
async function checkSessionValidity() {
|
||||
if (!sessionCookies) {
|
||||
return false;
|
||||
}
|
||||
const axios = require('axios');
|
||||
const headers = {
|
||||
"cookie": sessionCookies
|
||||
};
|
||||
try {
|
||||
const response = await axios.get('https://foodsharing.de/api/wall/foodsaver/839246?limit=1', { headers });
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Login-Funktion inkl. CSRF-Token-Extraktion
|
||||
async function login(userEmail, userPassword) {
|
||||
const axios = require('axios');
|
||||
const loginData = {
|
||||
email: userEmail,
|
||||
password: userPassword,
|
||||
remember_me: true
|
||||
};
|
||||
const headers = {
|
||||
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
|
||||
"Referer": "https://foodsharing.de/",
|
||||
"DNT": "1",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": "\"Windows\"",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
};
|
||||
|
||||
const response = await axios.post('https://foodsharing.de/api/user/login', loginData, { headers });
|
||||
if (response.status === 200) {
|
||||
sessionCookies = response.headers['set-cookie']; // Alle Cookies speichern
|
||||
csrfToken = extractCsrfToken(sessionCookies); // CSRF-Token extrahieren
|
||||
console.log("Login erfolgreich. CSRF Token: " + csrfToken);
|
||||
} else {
|
||||
throw new Error("Login fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion zum Senden der FoodSharing-Anfragen (zeigt Ergebnisse per Telegram an)
|
||||
function sendFoodSharingRequests(storeIds, session) {
|
||||
const axios = require('axios');
|
||||
let results = [];
|
||||
let errors = [];
|
||||
let shouldSendMessage = false;
|
||||
|
||||
storeIds.forEach(storeId => {
|
||||
const storeUrl = `https://foodsharing.de/api/map/stores/${storeId}`;
|
||||
const storeHeaders = {
|
||||
"cookie": session
|
||||
};
|
||||
|
||||
axios.get(storeUrl, { headers: storeHeaders })
|
||||
.then(storeResponse => {
|
||||
const storeData = storeResponse.data;
|
||||
if (storeData.maySendRequest) {
|
||||
shouldSendMessage = true;
|
||||
}
|
||||
results.push({
|
||||
ID: storeId,
|
||||
Name: masterStores[storeId] || storeData.name,
|
||||
AnmeldungOffen: storeData.maySendRequest ? "Ja" : "Nein"
|
||||
});
|
||||
|
||||
if (results.length + errors.length === storeIds.length) {
|
||||
if (shouldSendMessage || errors.length > 0) {
|
||||
sendResultsViaTelegram(results, errors);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
errors.push(`Fehler bei der Store-Anfrage f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${error.message}`);
|
||||
if (results.length + errors.length === storeIds.length) {
|
||||
sendResultsViaTelegram(results, errors);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Erweiterte Funktion zur Pr<50>fung der Pickup-Daten mit automatischer Buchung
|
||||
function checkPickupData(storeId, checkProfileId, session, onlyNotify = false, desiredWeekday = null, desiredDate = null) {
|
||||
const axios = require('axios');
|
||||
const storeUrl = `https://foodsharing.de/api/stores/${storeId}/pickups`;
|
||||
const headers = { "cookie": session };
|
||||
|
||||
axios.get(storeUrl, { headers })
|
||||
.then(response => {
|
||||
const pickups = response.data.pickups;
|
||||
let hasProfileId = false;
|
||||
let availablePickup = null;
|
||||
|
||||
pickups.forEach(pickup => {
|
||||
const pickupDate = new Date(pickup.date);
|
||||
const weekday = pickupDate.toLocaleDateString("en-US", { weekday: 'long' });
|
||||
|
||||
let matchesFilter = true;
|
||||
|
||||
if (desiredDate) {
|
||||
const desired = new Date(desiredDate);
|
||||
matchesFilter = pickupDate.getFullYear() === desired.getFullYear()
|
||||
&& pickupDate.getMonth() === desired.getMonth()
|
||||
&& pickupDate.getDate() === desired.getDate();
|
||||
}
|
||||
|
||||
if (desiredWeekday && !matchesFilter) {
|
||||
matchesFilter = weekday === desiredWeekday;
|
||||
}
|
||||
|
||||
if (!matchesFilter) return;
|
||||
|
||||
if (checkProfileId && pickup.occupiedSlots.some(slot => slot.profile.id === profileId)) {
|
||||
hasProfileId = true;
|
||||
} else if (pickup.isAvailable && !availablePickup) {
|
||||
availablePickup = pickup;
|
||||
}
|
||||
});
|
||||
|
||||
// Sonderfall TVS f<>r StoreID 51450
|
||||
if (storeId === "51450" && availablePickup && availablePickup.description === "TVS") {
|
||||
console.log(`Slot mit Beschreibung "TVS" gefunden f<>r Store ${storeId}. Buchung wird <20>bersprungen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!checkProfileId || !hasProfileId) && availablePickup) {
|
||||
const localDate = availablePickup.date;
|
||||
const utcDate = new Date(localDate).toISOString();
|
||||
const readableDate = new Date(localDate).toLocaleString("de-DE");
|
||||
|
||||
if (onlyNotify) {
|
||||
console.log(`Benachrichtigung: Freier Slot f<>r ${masterStores[storeId] || storeId} am ${readableDate}`);
|
||||
sendTelegramMessage(telegramInstance, `?? Freier Slot gefunden f<>r ${masterStores[storeId] || storeId} am ${readableDate} (nur Benachrichtigung).`);
|
||||
} else {
|
||||
bookSlot(storeId, utcDate, localDate);
|
||||
}
|
||||
} else {
|
||||
console.log(`Kein passender Slot f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}) gefunden.`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Fehler bei Pickup-Abfrage f<>r Store ${storeId}: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Funktion zum automatischen Buchen eines freien Slots
|
||||
function bookSlot(storeId, utcDate, localDate) {
|
||||
const axios = require('axios');
|
||||
console.log(`Starte Buchung f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate} (UTC: ${utcDate})...`);
|
||||
|
||||
// 1. Pickup-Rule Check durchf<68>hren
|
||||
axios.get(`https://foodsharing.de/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, {
|
||||
headers: {
|
||||
"cookie": sessionCookies,
|
||||
"x-csrf-token": csrfToken
|
||||
}
|
||||
})
|
||||
.then(ruleResponse => {
|
||||
if (ruleResponse.data.result === true) {
|
||||
// 2. Slot buchen, wenn Rule Check erfolgreich war
|
||||
axios.post(`https://foodsharing.de/api/stores/${storeId}/pickups/${utcDate}/${profileId}`, {}, {
|
||||
headers: {
|
||||
"cookie": sessionCookies,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"x-csrf-token": csrfToken
|
||||
}
|
||||
})
|
||||
.then(bookingResponse => {
|
||||
console.log("Buchung Response:", bookingResponse.data);
|
||||
sendTelegramMessage(telegramInstance, `Slot gebucht f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate}`);
|
||||
})
|
||||
.catch(bookingError => {
|
||||
console.error("Buchung Fehler:", bookingError.message);
|
||||
sendTelegramMessage(telegramInstance, `Buchungsfehler f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${bookingError.message}`);
|
||||
});
|
||||
} else {
|
||||
console.log("Pickup Rule Check fehlgeschlagen.");
|
||||
sendTelegramMessage(telegramInstance, `Pickup Rule Check fehlgeschlagen f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate}`);
|
||||
}
|
||||
})
|
||||
.catch(ruleError => {
|
||||
console.error("Fehler beim Pickup Rule Check:", ruleError.message);
|
||||
sendTelegramMessage(telegramInstance, `Fehler beim Pickup Rule Check f<>r Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${ruleError.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Funktion zum Senden der Ergebnisse und Fehler per Telegram
|
||||
function sendResultsViaTelegram(results, errors) {
|
||||
let message = "<b>Ergebnisse der FoodSharing-Anfragen:</b>\n\n";
|
||||
|
||||
if (results.length > 0) {
|
||||
message += "<b>Erfolg:</b>\n<pre>ID | Name | Anmeldung offen\n";
|
||||
message += "--------|------------------------------------|-----------------\n";
|
||||
results.forEach(result => {
|
||||
message += `${result.ID.padEnd(8)}| ${result.Name.padEnd(36)}| ${result.AnmeldungOffen}\n`;
|
||||
});
|
||||
message += "</pre>\n";
|
||||
} else {
|
||||
message += "Keine erfolgreichen Anfragen.\n";
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
message += "\n<b>Fehler:</b>\n";
|
||||
errors.forEach(error => {
|
||||
message += `${error}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
sendTelegramMessage(telegramInstance, message, true);
|
||||
}
|
||||
|
||||
// Funktion zum Senden einer Nachricht per Telegram
|
||||
function sendTelegramMessage(instance, message, isHtml = false) {
|
||||
const options = { text: message };
|
||||
if (isHtml) {
|
||||
options.parse_mode = 'HTML';
|
||||
}
|
||||
sendTo(instance, options);
|
||||
}
|
||||
|
||||
// Funktion zur Planung mit zuf<75>lliger Verz<72>gerung
|
||||
function scheduleWithRandomDelay(cronExpression, callback) {
|
||||
const job = schedule(cronExpression, function () {
|
||||
let delay = Math.floor(Math.random() * 110 * 1000) + 10 * 1000; // 10s bis 120s
|
||||
setTimeout(callback, delay);
|
||||
});
|
||||
return job;
|
||||
}
|
||||
|
||||
// Zuf<75>lliger Start des Skripts
|
||||
startScriptWithRandomDelay();
|
||||
|
||||
// Zeitplan: T<>glich um 7 Uhr morgens bzw. 15 Uhr (hier per Cron-Ausdruck)
|
||||
schedule('0 7,15 * * *', function () {
|
||||
startScriptWithRandomDelay(true);
|
||||
});
|
||||
|
||||
// Initialisierung
|
||||
setupPickupCheckSchedules();
|
||||
|
||||
// Trigger bei <20>nderungen des Datenpunkts
|
||||
on({ id: configState, change: "any" }, () => {
|
||||
console.log("?? Konfiguration ge<67>ndert. Pickup-Checks werden neu geladen.");
|
||||
setupPickupCheckSchedules();
|
||||
});
|
||||
546
package-lock.json
generated
546
package-lock.json
generated
@@ -12,12 +12,14 @@
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"mqtt": "^5.3.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"uuid": "^11.0.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -87,6 +89,7 @@
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -831,6 +834,7 @@
|
||||
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
},
|
||||
@@ -1753,6 +1757,7 @@
|
||||
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
@@ -3804,6 +3809,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -4167,6 +4173,7 @@
|
||||
"version": "22.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
|
||||
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -4217,22 +4224,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/readable-stream": {
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
|
||||
"integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"safe-buffer": "~5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@@ -4871,18 +4862,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -4902,6 +4881,7 @@
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5021,6 +5001,7 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -5429,7 +5410,6 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
@@ -5506,6 +5486,33 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -5793,26 +5800,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
@@ -5860,18 +5847,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz",
|
||||
"integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
@@ -5972,6 +5947,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
@@ -5995,34 +5971,11 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
@@ -6469,7 +6422,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@@ -6488,12 +6440,6 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/commist": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/common-tags": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
|
||||
@@ -6560,35 +6506,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
@@ -7327,7 +7244,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -7868,7 +7784,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -7980,6 +7895,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -8704,15 +8620,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
@@ -8724,6 +8631,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
@@ -8875,19 +8783,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-unique-numbers": {
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz",
|
||||
"integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.8",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
@@ -9123,7 +9018,6 @@
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -9779,7 +9673,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -9813,12 +9706,6 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hoopy": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
||||
@@ -10198,26 +10085,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -10345,25 +10212,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -11112,6 +10960,7 @@
|
||||
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^27.5.1",
|
||||
"import-local": "^3.0.2",
|
||||
@@ -12065,16 +11914,6 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -12095,12 +11934,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "16.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
|
||||
@@ -12513,6 +12346,7 @@
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
@@ -12754,6 +12588,7 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -12782,93 +12617,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.13.0.tgz",
|
||||
"integrity": "sha512-pR+z+ChxFl3n8AKLQbTONVOOg/jl4KiKQRBAi78tjd6PksOWvl1nl9L8ZHOZ3MiavZfrUOjok2ddwc1VymGWRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commist": "^3.2.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"debug": "^4.4.0",
|
||||
"help-me": "^5.0.0",
|
||||
"lru-cache": "^10.4.3",
|
||||
"minimist": "^1.2.8",
|
||||
"mqtt-packet": "^9.0.2",
|
||||
"number-allocator": "^1.0.14",
|
||||
"readable-stream": "^4.7.0",
|
||||
"rfdc": "^1.4.1",
|
||||
"socks": "^2.8.3",
|
||||
"split2": "^4.2.0",
|
||||
"worker-timers": "^7.1.8",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"bin": {
|
||||
"mqtt": "build/bin/mqtt.js",
|
||||
"mqtt_pub": "build/bin/pub.js",
|
||||
"mqtt_sub": "build/bin/sub.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^6.0.8",
|
||||
"debug": "^4.3.4",
|
||||
"process-nextick-args": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mqtt/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -12961,6 +12709,27 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
|
||||
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
@@ -13044,39 +12813,6 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/number-allocator": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"js-sdsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/number-allocator/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/number-allocator/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.20",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||
@@ -13698,6 +13434,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -14956,6 +14693,7 @@
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -15127,19 +14865,11 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/promise": {
|
||||
@@ -15198,6 +14928,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||
@@ -15325,6 +15061,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -15463,6 +15200,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -15489,6 +15227,7 @@
|
||||
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -15577,22 +15316,6 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -15958,12 +15681,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -15987,6 +15704,7 @@
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -16242,6 +15960,7 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -16627,16 +16346,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs": {
|
||||
"version": "0.3.24",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
|
||||
@@ -16649,18 +16358,14 @@
|
||||
"websocket-driver": "^0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
||||
"node_modules/sockjs/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/source-list-map": {
|
||||
@@ -16851,15 +16556,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -17026,6 +16722,7 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
@@ -18008,6 +17705,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
@@ -18062,6 +17760,7 @@
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -18160,12 +17859,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
@@ -18177,9 +17870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
@@ -18188,7 +17881,7 @@
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
@@ -18221,6 +17914,7 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@@ -18373,6 +18067,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util.promisify": {
|
||||
@@ -18408,13 +18103,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
@@ -18528,6 +18226,7 @@
|
||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
@@ -18600,6 +18299,7 @@
|
||||
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.9",
|
||||
"@types/connect-history-api-fallback": "^1.3.5",
|
||||
@@ -19012,6 +18712,7 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -19268,40 +18969,6 @@
|
||||
"workbox-core": "6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers": {
|
||||
"version": "7.1.8",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz",
|
||||
"integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.5",
|
||||
"tslib": "^2.6.2",
|
||||
"worker-timers-broker": "^6.1.8",
|
||||
"worker-timers-worker": "^7.0.71"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers-broker": {
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz",
|
||||
"integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.5",
|
||||
"fast-unique-numbers": "^8.0.13",
|
||||
"tslib": "^2.6.2",
|
||||
"worker-timers-worker": "^7.0.71"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers-worker": {
|
||||
"version": "7.0.71",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz",
|
||||
"integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.5",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -19363,6 +19030,7 @@
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -10,9 +11,10 @@
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"mqtt": "^5.3.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"uuid": "^11.0.3",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
362
server.js
362
server.js
@@ -1,126 +1,290 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
const mqtt = require('mqtt');
|
||||
|
||||
const sessionStore = require('./services/sessionStore');
|
||||
const credentialStore = require('./services/credentialStore');
|
||||
const { readConfig, writeConfig } = require('./services/configStore');
|
||||
const foodsharingClient = require('./services/foodsharingClient');
|
||||
const { scheduleConfig } = require('./services/pickupScheduler');
|
||||
const adminConfig = require('./services/adminConfig');
|
||||
|
||||
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
||||
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const mqttBroker = process.env.MQTT_BROKER || 'mqtt://192.168.1.100:1883';
|
||||
const mqttTopic = process.env.MQTT_TOPIC || 'iobroker/pickupCheck/config';
|
||||
const mqttUser = process.env.MQTT_USER || 'iobroker';
|
||||
const mqttPassword = process.env.MQTT_PASSWORD || 'password';
|
||||
const configPath = './config/pickup-config.json';
|
||||
|
||||
// Logger mit Timestamp
|
||||
function logWithTimestamp(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
function errorWithTimestamp(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}]`, ...args);
|
||||
}
|
||||
|
||||
// MQTT-Client mit Authentifizierung initialisieren
|
||||
const mqttClient = mqtt.connect(mqttBroker, {
|
||||
clientId: 'pickup-config-web-' + Math.random().toString(16).substring(2, 8),
|
||||
clean: true,
|
||||
username: mqttUser,
|
||||
password: mqttPassword
|
||||
});
|
||||
|
||||
// MQTT-Events
|
||||
mqttClient.on('connect', () => {
|
||||
logWithTimestamp('Verbunden mit MQTT-Broker:', mqttBroker);
|
||||
|
||||
mqttClient.subscribe(mqttTopic, (err) => {
|
||||
if (!err) {
|
||||
logWithTimestamp('Abonniert auf Topic:', mqttTopic);
|
||||
mqttClient.publish(mqttTopic + '/get', 'true');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mqttClient.on('error', (error) => {
|
||||
errorWithTimestamp('MQTT-Fehler:', error);
|
||||
});
|
||||
|
||||
mqttClient.on('message', (topic, message) => {
|
||||
logWithTimestamp('Nachricht erhalten auf Topic:', topic);
|
||||
|
||||
if (topic === mqttTopic) {
|
||||
try {
|
||||
const config = JSON.parse(message.toString());
|
||||
logWithTimestamp('Konfiguration vom MQTT-Broker erhalten');
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Fehler beim Verarbeiten der MQTT-Nachricht:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'build')));
|
||||
|
||||
// Sicherstellen, dass Konfigurationsordner existiert
|
||||
const configDir = path.dirname(configPath);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
function isAdmin(profile) {
|
||||
if (!adminEmail || !profile?.email) {
|
||||
return false;
|
||||
}
|
||||
return profile.email.toLowerCase() === adminEmail;
|
||||
}
|
||||
|
||||
// Initiale Konfigurationsdatei erstellen, falls nicht vorhanden
|
||||
if (!fs.existsSync(configPath)) {
|
||||
const initialConfig = [
|
||||
{ id: "63448", active: false, checkProfileId: true, onlyNotify: true, label: "Penny Baden-Oos" },
|
||||
{ id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredWeekday: "Samstag" },
|
||||
{ id: "44972", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Biblisweg", desiredWeekday: "Dienstag" },
|
||||
{ id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredDate: "2025-05-18" },
|
||||
{ id: "33875", active: false, checkProfileId: true, onlyNotify: false, label: "Cap Markt", desiredWeekday: "Donnerstag" },
|
||||
{ id: "42322", active: false, checkProfileId: false, onlyNotify: false, label: "Edeka Haueneberstein" },
|
||||
{ id: "51450", active: false, checkProfileId: true, onlyNotify: false, label: "Hornbach Grünwinkel" }
|
||||
];
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2));
|
||||
mqttClient.publish(mqttTopic, JSON.stringify(initialConfig));
|
||||
logWithTimestamp('Initiale Konfiguration erstellt und an MQTT gesendet');
|
||||
function scheduleWithCurrentSettings(sessionId, config) {
|
||||
const settings = adminConfig.readSettings();
|
||||
scheduleConfig(sessionId, config, settings);
|
||||
}
|
||||
|
||||
// API: Konfiguration abrufen
|
||||
app.get('/api/iobroker/pickup-config', (req, res) => {
|
||||
try {
|
||||
const configData = fs.readFileSync(configPath, 'utf8');
|
||||
res.json(JSON.parse(configData));
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Error reading configuration:', error);
|
||||
res.status(500).json({ error: 'Failed to read configuration' });
|
||||
function rescheduleAllSessions() {
|
||||
const settings = adminConfig.readSettings();
|
||||
sessionStore.list().forEach((session) => {
|
||||
if (!session?.profile?.id) {
|
||||
return;
|
||||
}
|
||||
const config = readConfig(session.profile.id);
|
||||
scheduleConfig(session.id, config, settings);
|
||||
});
|
||||
}
|
||||
|
||||
function mergeStoresIntoConfig(config = [], stores = []) {
|
||||
const entries = Array.isArray(config) ? config : [];
|
||||
const map = new Map();
|
||||
entries.forEach((entry) => {
|
||||
if (!entry || !entry.id) {
|
||||
return;
|
||||
}
|
||||
map.set(String(entry.id), { ...entry, id: String(entry.id) });
|
||||
});
|
||||
|
||||
let changed = false;
|
||||
stores.forEach((store) => {
|
||||
if (!store?.id) {
|
||||
return;
|
||||
}
|
||||
const id = String(store.id);
|
||||
if (!map.has(id)) {
|
||||
map.set(id, {
|
||||
id,
|
||||
label: store.name || `Store ${id}`,
|
||||
active: false,
|
||||
checkProfileId: true,
|
||||
onlyNotify: false,
|
||||
hidden: false
|
||||
});
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = map.get(id);
|
||||
if (!existing.label && store.name) {
|
||||
existing.label = store.name;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// API: Konfiguration speichern
|
||||
app.post('/api/iobroker/pickup-config', (req, res) => {
|
||||
return { merged: Array.from(map.values()), changed };
|
||||
}
|
||||
|
||||
async function restoreSessionsFromDisk() {
|
||||
const saved = credentialStore.loadAll();
|
||||
const entries = Object.entries(saved);
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[RESTORE] Versuche ${entries.length} gespeicherte Anmeldung(en) zu laden...`);
|
||||
const schedulerSettings = adminConfig.readSettings();
|
||||
|
||||
for (const [profileId, credentials] of entries) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const newConfig = req.body;
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
||||
mqttClient.publish(mqttTopic, JSON.stringify(newConfig));
|
||||
logWithTimestamp('Konfiguration über MQTT gesendet');
|
||||
const auth = await foodsharingClient.login(credentials.email, credentials.password);
|
||||
const profile = {
|
||||
id: String(auth.profile.id),
|
||||
name: auth.profile.name,
|
||||
email: auth.profile.email || credentials.email
|
||||
};
|
||||
const isAdminUser = isAdmin(profile);
|
||||
let config = readConfig(profile.id);
|
||||
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
|
||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||
if (changed) {
|
||||
config = merged;
|
||||
writeConfig(profile.id, config);
|
||||
}
|
||||
|
||||
const session = sessionStore.create({
|
||||
cookieHeader: auth.cookieHeader,
|
||||
csrfToken: auth.csrfToken,
|
||||
profile,
|
||||
credentials,
|
||||
isAdmin: isAdminUser
|
||||
}, credentials.token, ONE_YEAR_MS);
|
||||
credentialStore.save(profile.id, {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
token: session.id
|
||||
});
|
||||
scheduleConfig(session.id, config, schedulerSettings);
|
||||
console.log(`[RESTORE] Session fuer Profil ${profile.id} (${profile.name}) reaktiviert.`);
|
||||
} catch (error) {
|
||||
console.error(`[RESTORE] Login fuer Profil ${profileId} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const [scheme, token] = header.split(' ');
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return res.status(401).json({ error: 'Unautorisiert' });
|
||||
}
|
||||
|
||||
const session = sessionStore.get(token);
|
||||
if (!session) {
|
||||
return res.status(401).json({ error: 'Session nicht gefunden oder abgelaufen' });
|
||||
}
|
||||
|
||||
req.session = session;
|
||||
next();
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.session?.isAdmin) {
|
||||
return res.status(403).json({ error: 'Nur für Admins verfügbar' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' });
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = await foodsharingClient.login(email, password);
|
||||
const profile = {
|
||||
id: String(auth.profile.id),
|
||||
name: auth.profile.name,
|
||||
email: auth.profile.email || email
|
||||
};
|
||||
const isAdminUser = isAdmin(profile);
|
||||
|
||||
let config = readConfig(profile.id);
|
||||
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
|
||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||
if (changed) {
|
||||
config = merged;
|
||||
writeConfig(profile.id, config);
|
||||
}
|
||||
|
||||
const existingCredentials = credentialStore.get(profile.id);
|
||||
const existingToken = existingCredentials?.token;
|
||||
if (existingToken) {
|
||||
sessionStore.delete(existingToken);
|
||||
}
|
||||
|
||||
const session = sessionStore.create({
|
||||
cookieHeader: auth.cookieHeader,
|
||||
csrfToken: auth.csrfToken,
|
||||
profile,
|
||||
credentials: { email, password },
|
||||
isAdmin: isAdminUser
|
||||
}, existingToken, ONE_YEAR_MS);
|
||||
|
||||
credentialStore.save(profile.id, { email, password, token: session.id });
|
||||
const settings = adminConfig.readSettings();
|
||||
scheduleConfig(session.id, config, settings);
|
||||
|
||||
return res.json({
|
||||
token: session.id,
|
||||
profile,
|
||||
stores,
|
||||
config,
|
||||
isAdmin: isAdminUser,
|
||||
adminSettings: isAdminUser ? settings : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login fehlgeschlagen:', error.message);
|
||||
return res.status(401).json({ error: 'Login fehlgeschlagen' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', requireAuth, (req, res) => {
|
||||
sessionStore.delete(req.session.id);
|
||||
credentialStore.remove(req.session.profile.id);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Error saving configuration:', error);
|
||||
res.status(500).json({ error: 'Failed to save configuration' });
|
||||
}
|
||||
});
|
||||
|
||||
// React-App ausliefern
|
||||
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
||||
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
|
||||
let config = readConfig(req.session.profile.id);
|
||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||
if (changed) {
|
||||
config = merged;
|
||||
writeConfig(req.session.profile.id, config);
|
||||
}
|
||||
res.json({
|
||||
profile: req.session.profile,
|
||||
stores,
|
||||
isAdmin: !!req.session.isAdmin,
|
||||
adminSettings: req.session.isAdmin ? adminConfig.readSettings() : undefined
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/profile', requireAuth, async (req, res) => {
|
||||
const details = await foodsharingClient.fetchProfile(req.session.cookieHeader);
|
||||
res.json({
|
||||
profile: details || req.session.profile
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/config', requireAuth, (req, res) => {
|
||||
const config = readConfig(req.session.profile.id);
|
||||
res.json(config);
|
||||
});
|
||||
|
||||
app.post('/api/config', requireAuth, (req, res) => {
|
||||
if (!Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: 'Konfiguration muss ein Array sein' });
|
||||
}
|
||||
writeConfig(req.session.profile.id, req.body);
|
||||
scheduleWithCurrentSettings(req.session.id, req.body);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.get('/api/stores', requireAuth, async (req, res) => {
|
||||
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
|
||||
let config = readConfig(req.session.profile.id);
|
||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||
if (changed) {
|
||||
config = merged;
|
||||
writeConfig(req.session.profile.id, config);
|
||||
}
|
||||
res.json(stores);
|
||||
});
|
||||
|
||||
app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {
|
||||
res.json(adminConfig.readSettings());
|
||||
});
|
||||
|
||||
app.post('/api/admin/settings', requireAuth, requireAdmin, (req, res) => {
|
||||
const updated = adminConfig.writeSettings(req.body || {});
|
||||
rescheduleAllSessions();
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
||||
});
|
||||
|
||||
// Server starten
|
||||
app.listen(port, () => {
|
||||
logWithTimestamp(`Server läuft auf Port ${port}`);
|
||||
console.log(`Server läuft auf Port ${port}`);
|
||||
});
|
||||
|
||||
restoreSessionsFromDisk().catch((error) => {
|
||||
console.error('[RESTORE] Fehler bei der Session-Wiederherstellung:', error.message);
|
||||
});
|
||||
|
||||
94
services/adminConfig.js
Normal file
94
services/adminConfig.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
const SETTINGS_FILE = path.join(CONFIG_DIR, 'admin-settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
scheduleCron: '*/10 7-22 * * *',
|
||||
randomDelayMinSeconds: 10,
|
||||
randomDelayMaxSeconds: 120,
|
||||
initialDelayMinSeconds: 5,
|
||||
initialDelayMaxSeconds: 30,
|
||||
ignoredSlots: [
|
||||
{
|
||||
storeId: '51450',
|
||||
description: 'TVS'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeNumber(value, fallback) {
|
||||
const num = Number(value);
|
||||
if (Number.isFinite(num) && num >= 0) {
|
||||
return num;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function sanitizeIgnoredSlots(slots = []) {
|
||||
if (!Array.isArray(slots)) {
|
||||
return DEFAULT_SETTINGS.ignoredSlots;
|
||||
}
|
||||
return slots
|
||||
.map((slot) => ({
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
description: slot?.description ? String(slot.description) : ''
|
||||
}))
|
||||
.filter((slot) => slot.storeId);
|
||||
}
|
||||
|
||||
function readSettings() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(DEFAULT_SETTINGS, null, 2));
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
scheduleCron: parsed.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
|
||||
randomDelayMinSeconds: sanitizeNumber(parsed.randomDelayMinSeconds, DEFAULT_SETTINGS.randomDelayMinSeconds),
|
||||
randomDelayMaxSeconds: sanitizeNumber(parsed.randomDelayMaxSeconds, DEFAULT_SETTINGS.randomDelayMaxSeconds),
|
||||
initialDelayMinSeconds: sanitizeNumber(parsed.initialDelayMinSeconds, DEFAULT_SETTINGS.initialDelayMinSeconds),
|
||||
initialDelayMaxSeconds: sanitizeNumber(parsed.initialDelayMaxSeconds, DEFAULT_SETTINGS.initialDelayMaxSeconds),
|
||||
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Konnte Admin-Einstellungen nicht lesen:', error.message);
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
function writeSettings(patch = {}) {
|
||||
const current = readSettings();
|
||||
const next = {
|
||||
scheduleCron: patch.scheduleCron || current.scheduleCron,
|
||||
randomDelayMinSeconds: sanitizeNumber(patch.randomDelayMinSeconds, current.randomDelayMinSeconds),
|
||||
randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds),
|
||||
initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds),
|
||||
initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds),
|
||||
ignoredSlots:
|
||||
patch.ignoredSlots !== undefined
|
||||
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
||||
: current.ignoredSlots
|
||||
};
|
||||
|
||||
ensureDir();
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2));
|
||||
return next;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SETTINGS,
|
||||
readSettings,
|
||||
writeSettings
|
||||
};
|
||||
48
services/configStore.js
Normal file
48
services/configStore.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const defaultConfig = require('../data/defaultConfig');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigPath(profileId = 'shared') {
|
||||
return path.join(CONFIG_DIR, `${profileId}-pickup-config.json`);
|
||||
}
|
||||
|
||||
function hydrateConfigFile(profileId) {
|
||||
ensureDir();
|
||||
const filePath = getConfigPath(profileId);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function readConfig(profileId) {
|
||||
const filePath = hydrateConfigFile(profileId);
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (err) {
|
||||
console.error(`Failed to read config for ${profileId}:`, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(profileId, payload) {
|
||||
const filePath = hydrateConfigFile(profileId);
|
||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
||||
return filePath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readConfig,
|
||||
writeConfig,
|
||||
getConfigPath
|
||||
};
|
||||
71
services/credentialStore.js
Normal file
71
services/credentialStore.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
const CREDENTIAL_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readStore() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(CREDENTIAL_FILE)) {
|
||||
fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify({}, null, 2));
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(CREDENTIAL_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch (error) {
|
||||
console.error('Konnte Credential-Store nicht lesen:', error.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store) {
|
||||
ensureDir();
|
||||
fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
function save(profileId, credentials) {
|
||||
if (!profileId || !credentials?.email || !credentials?.password) {
|
||||
return;
|
||||
}
|
||||
const store = readStore();
|
||||
store[profileId] = credentials;
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
function remove(profileId) {
|
||||
if (!profileId) {
|
||||
return;
|
||||
}
|
||||
const store = readStore();
|
||||
if (store[profileId]) {
|
||||
delete store[profileId];
|
||||
writeStore(store);
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
return readStore();
|
||||
}
|
||||
|
||||
function get(profileId) {
|
||||
if (!profileId) {
|
||||
return null;
|
||||
}
|
||||
const store = readStore();
|
||||
return store[profileId] || null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
save,
|
||||
remove,
|
||||
loadAll,
|
||||
get
|
||||
};
|
||||
179
services/foodsharingClient.js
Normal file
179
services/foodsharingClient.js
Normal file
@@ -0,0 +1,179 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://foodsharing.de';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'User-Agent': 'pickup-config/1.0 (+https://foodsharing.de)',
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
});
|
||||
|
||||
function extractCsrfToken(cookies = []) {
|
||||
if (!Array.isArray(cookies)) {
|
||||
return null;
|
||||
}
|
||||
const tokenCookie = cookies.find((cookie) => cookie.startsWith('CSRF_TOKEN='));
|
||||
if (!tokenCookie) {
|
||||
return null;
|
||||
}
|
||||
return tokenCookie.split(';')[0].split('=')[1];
|
||||
}
|
||||
|
||||
function serializeCookies(cookies = []) {
|
||||
if (!Array.isArray(cookies)) {
|
||||
return '';
|
||||
}
|
||||
return cookies.map((c) => c.split(';')[0]).join('; ');
|
||||
}
|
||||
|
||||
function buildHeaders(cookieHeader, csrfToken) {
|
||||
const headers = {};
|
||||
if (cookieHeader) {
|
||||
headers.cookie = cookieHeader;
|
||||
}
|
||||
if (csrfToken) {
|
||||
headers['x-csrf-token'] = csrfToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function getCurrentUserDetails(cookieHeader) {
|
||||
const response = await client.get('/api/user/current/details', {
|
||||
headers: buildHeaders(cookieHeader)
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function login(email, password) {
|
||||
const payload = {
|
||||
email,
|
||||
password,
|
||||
remember_me: true
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
|
||||
Referer: BASE_URL,
|
||||
DNT: '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Linux"',
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
};
|
||||
|
||||
const response = await client.post('/api/user/login', payload, { headers });
|
||||
const cookies = response.headers['set-cookie'] || [];
|
||||
const csrfToken = extractCsrfToken(cookies);
|
||||
const cookieHeader = serializeCookies(cookies);
|
||||
const details = await getCurrentUserDetails(cookieHeader);
|
||||
if (!details?.id) {
|
||||
throw new Error('Profil-ID konnte nicht ermittelt werden.');
|
||||
}
|
||||
|
||||
const nameParts = [];
|
||||
if (details.firstname) {
|
||||
nameParts.push(details.firstname);
|
||||
}
|
||||
if (details.lastname) {
|
||||
nameParts.push(details.lastname);
|
||||
}
|
||||
|
||||
return {
|
||||
csrfToken,
|
||||
cookieHeader,
|
||||
profile: {
|
||||
id: String(details.id),
|
||||
name: nameParts.length > 0 ? nameParts.join(' ') : details.email || email,
|
||||
email: details.email || email
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function checkSession(cookieHeader, profileId) {
|
||||
if (!cookieHeader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await client.get(`/api/wall/foodsaver/${profileId}?limit=1`, {
|
||||
headers: buildHeaders(cookieHeader)
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfile(cookieHeader) {
|
||||
try {
|
||||
return await getCurrentUserDetails(cookieHeader);
|
||||
} catch (error) {
|
||||
console.warn('Profil konnte nicht geladen werden:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStores(cookieHeader, profileId) {
|
||||
if (!profileId) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const response = await client.get(`/api/user/${profileId}/stores`, {
|
||||
headers: buildHeaders(cookieHeader),
|
||||
params: { activeStores: 1 }
|
||||
});
|
||||
const stores = response.data || [];
|
||||
if (!Array.isArray(stores)) {
|
||||
return [];
|
||||
}
|
||||
return stores.map((store) => ({
|
||||
id: String(store.id),
|
||||
name: store.name || `Store ${store.id}`,
|
||||
pickupStatus: store.pickupStatus,
|
||||
membershipStatus: store.membershipStatus,
|
||||
isManaging: !!store.isManaging,
|
||||
city: store.city || '',
|
||||
street: store.street || '',
|
||||
zip: store.zip || ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Stores konnten nicht geladen werden:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPickups(storeId, cookieHeader) {
|
||||
const response = await client.get(`/api/stores/${storeId}/pickups`, {
|
||||
headers: buildHeaders(cookieHeader)
|
||||
});
|
||||
return response.data?.pickups || [];
|
||||
}
|
||||
|
||||
async function pickupRuleCheck(storeId, utcDate, profileId, session) {
|
||||
const response = await client.get(`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, {
|
||||
headers: buildHeaders(session.cookieHeader, session.csrfToken)
|
||||
});
|
||||
return response.data?.result === true;
|
||||
}
|
||||
|
||||
async function bookSlot(storeId, utcDate, profileId, session) {
|
||||
await client.post(
|
||||
`/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
|
||||
{},
|
||||
{
|
||||
headers: buildHeaders(session.cookieHeader, session.csrfToken)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
checkSession,
|
||||
fetchProfile,
|
||||
fetchStores,
|
||||
fetchPickups,
|
||||
pickupRuleCheck,
|
||||
bookSlot
|
||||
};
|
||||
236
services/pickupScheduler.js
Normal file
236
services/pickupScheduler.js
Normal file
@@ -0,0 +1,236 @@
|
||||
const cron = require('node-cron');
|
||||
const foodsharingClient = require('./foodsharingClient');
|
||||
const sessionStore = require('./sessionStore');
|
||||
const { DEFAULT_SETTINGS } = require('./adminConfig');
|
||||
|
||||
const weekdayMap = {
|
||||
Montag: 'Monday',
|
||||
Dienstag: 'Tuesday',
|
||||
Mittwoch: 'Wednesday',
|
||||
Donnerstag: 'Thursday',
|
||||
Freitag: 'Friday',
|
||||
Samstag: 'Saturday',
|
||||
Sonntag: 'Sunday'
|
||||
};
|
||||
|
||||
function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
|
||||
const min = minSeconds * 1000;
|
||||
const max = maxSeconds * 1000;
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function resolveSettings(settings) {
|
||||
if (!settings) {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
return {
|
||||
scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
|
||||
randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
|
||||
? settings.randomDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMinSeconds,
|
||||
randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
|
||||
? settings.randomDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMaxSeconds,
|
||||
initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
|
||||
? settings.initialDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMinSeconds,
|
||||
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
|
||||
? settings.initialDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
|
||||
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSession(session) {
|
||||
const profileId = session.profile?.id;
|
||||
if (!profileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stillValid = await foodsharingClient.checkSession(session.cookieHeader, profileId);
|
||||
if (stillValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!session.credentials) {
|
||||
console.warn(`Session ${session.id} kann nicht erneuert werden – keine Zugangsdaten gespeichert.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshed = await foodsharingClient.login(
|
||||
session.credentials.email,
|
||||
session.credentials.password
|
||||
);
|
||||
sessionStore.update(session.id, {
|
||||
cookieHeader: refreshed.cookieHeader,
|
||||
csrfToken: refreshed.csrfToken,
|
||||
profile: {
|
||||
...session.profile,
|
||||
...refreshed.profile
|
||||
}
|
||||
});
|
||||
console.log(`Session ${session.id} wurde erfolgreich erneuert.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Session ${session.id} konnte nicht erneuert werden:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesDesiredDate(pickupDate, desiredDate) {
|
||||
if (!desiredDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const desired = new Date(desiredDate);
|
||||
return (
|
||||
pickupDate.getFullYear() === desired.getFullYear() &&
|
||||
pickupDate.getMonth() === desired.getMonth() &&
|
||||
pickupDate.getDate() === desired.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
|
||||
if (!desiredWeekday) {
|
||||
return true;
|
||||
}
|
||||
const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
|
||||
return weekday === desiredWeekday;
|
||||
}
|
||||
|
||||
function shouldIgnoreSlot(entry, pickup, settings) {
|
||||
const rules = settings.ignoredSlots || [];
|
||||
return rules.some((rule) => {
|
||||
if (!rule?.storeId) {
|
||||
return false;
|
||||
}
|
||||
if (String(rule.storeId) !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
if (rule.description) {
|
||||
return pickup.description === rule.description;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function processBooking(session, entry, pickup) {
|
||||
const readableDate = new Date(pickup.date).toLocaleString('de-DE');
|
||||
const storeName = entry.label || entry.id;
|
||||
|
||||
if (entry.onlyNotify) {
|
||||
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const utcDate = new Date(pickup.date).toISOString();
|
||||
try {
|
||||
const allowed = await foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session);
|
||||
if (!allowed) {
|
||||
console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
|
||||
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEntry(sessionId, entry, settings) {
|
||||
const session = sessionStore.get(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ready = await ensureSession(session);
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pickups = await foodsharingClient.fetchPickups(entry.id, session.cookieHeader);
|
||||
let hasProfileId = false;
|
||||
let availablePickup = null;
|
||||
|
||||
const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;
|
||||
|
||||
pickups.forEach((pickup) => {
|
||||
const pickupDate = new Date(pickup.date);
|
||||
if (!matchesDesiredDate(pickupDate, entry.desiredDate)) {
|
||||
return;
|
||||
}
|
||||
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
||||
return;
|
||||
}
|
||||
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
|
||||
hasProfileId = true;
|
||||
return;
|
||||
}
|
||||
if (pickup.isAvailable && !availablePickup) {
|
||||
availablePickup = pickup;
|
||||
}
|
||||
});
|
||||
|
||||
if (!availablePickup) {
|
||||
console.log(
|
||||
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
|
||||
hasProfileId ? 'ja' : 'nein'
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldIgnoreSlot(entry, availablePickup, settings)) {
|
||||
console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.checkProfileId || !hasProfileId) {
|
||||
await processBooking(session, entry, availablePickup);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleEntry(sessionId, entry, settings) {
|
||||
const cronExpression = settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron;
|
||||
const job = cron.schedule(
|
||||
cronExpression,
|
||||
() => {
|
||||
const delay = randomDelayMs(
|
||||
settings.randomDelayMinSeconds,
|
||||
settings.randomDelayMaxSeconds
|
||||
);
|
||||
setTimeout(() => checkEntry(sessionId, entry, settings), delay);
|
||||
},
|
||||
{
|
||||
timezone: 'Europe/Berlin'
|
||||
}
|
||||
);
|
||||
sessionStore.attachJob(sessionId, job);
|
||||
setTimeout(
|
||||
() => checkEntry(sessionId, entry, settings),
|
||||
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleConfig(sessionId, config, settings) {
|
||||
const resolvedSettings = resolveSettings(settings);
|
||||
sessionStore.clearJobs(sessionId);
|
||||
const activeEntries = config.filter((entry) => entry.active);
|
||||
if (activeEntries.length === 0) {
|
||||
console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} – Scheduler ruht.`);
|
||||
return;
|
||||
}
|
||||
activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
|
||||
console.log(
|
||||
`[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv (Cron: ${resolvedSettings.scheduleCron}).`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scheduleConfig
|
||||
};
|
||||
98
services/sessionStore.js
Normal file
98
services/sessionStore.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
class SessionStore {
|
||||
constructor() {
|
||||
this.sessions = new Map();
|
||||
this.profileIndex = new Map();
|
||||
}
|
||||
|
||||
create(payload, customId, ttlMs) {
|
||||
const id = customId || uuid();
|
||||
const profileId = payload?.profile?.id ? String(payload.profile.id) : null;
|
||||
|
||||
if (profileId && this.profileIndex.has(profileId)) {
|
||||
const previousId = this.profileIndex.get(profileId);
|
||||
if (previousId && previousId !== id) {
|
||||
this.delete(previousId);
|
||||
}
|
||||
}
|
||||
|
||||
const session = {
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
expiresAt: ttlMs ? Date.now() + ttlMs : null,
|
||||
jobs: [],
|
||||
...payload
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
|
||||
if (profileId) {
|
||||
this.profileIndex.set(profileId, id);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.expiresAt && session.expiresAt < Date.now()) {
|
||||
this.delete(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
update(id, patch) {
|
||||
const session = this.get(id);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
Object.assign(session, patch, { updatedAt: Date.now() });
|
||||
return session;
|
||||
}
|
||||
|
||||
attachJob(id, job) {
|
||||
const session = this.get(id);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
session.jobs.push(job);
|
||||
}
|
||||
|
||||
clearJobs(id) {
|
||||
const session = this.get(id);
|
||||
if (!session || !Array.isArray(session.jobs)) {
|
||||
return;
|
||||
}
|
||||
session.jobs.forEach((job) => {
|
||||
if (job && typeof job.stop === 'function') {
|
||||
job.stop();
|
||||
}
|
||||
});
|
||||
session.jobs = [];
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
const session = this.sessions.get(id);
|
||||
if (session) {
|
||||
this.clearJobs(id);
|
||||
const profileId = session.profile?.id ? String(session.profile.id) : null;
|
||||
if (profileId && this.profileIndex.get(profileId) === id) {
|
||||
this.profileIndex.delete(profileId);
|
||||
}
|
||||
}
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
|
||||
list() {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SessionStore();
|
||||
993
src/App.js
993
src/App.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user