aktueller Stand

This commit is contained in:
root
2025-11-09 13:50:17 +01:00
parent 81e7f94817
commit dc95156793
16 changed files with 2152 additions and 1148 deletions

6
.env
View File

@@ -1,9 +1,5 @@
# MQTT-Konfiguration # MQTT-Konfiguration
MQTT_BROKER=mqtt://iobroker:1884 ADMIN_EMAIL=meikdre@gmx.de
MQTT_TOPIC=foodsharing/pickupCheck/config/meik
MQTT_USER=mqtt
MQTT_PASSWORD=mqtt!1884!
# Server-Konfiguration # Server-Konfiguration
PORT=3000 PORT=3000
NODE_ENV=production NODE_ENV=production

View 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
}
]

View 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
View 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
View File

@@ -0,0 +1 @@
module.exports = [];

View File

@@ -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
View File

@@ -12,12 +12,14 @@
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.7",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"mqtt": "^5.3.0", "node-cron": "^3.0.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"uuid": "^11.0.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -87,6 +89,7 @@
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -831,6 +834,7 @@
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1753,6 +1757,7 @@
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-annotate-as-pure": "^7.27.1",
"@babel/helper-module-imports": "^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", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
@@ -4167,6 +4173,7 @@
"version": "22.15.18", "version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@@ -4217,22 +4224,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/resolve": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -4871,18 +4862,6 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -4902,6 +4881,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5021,6 +5001,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -5429,7 +5410,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
@@ -5506,6 +5486,33 @@
"node": ">=4" "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": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5793,26 +5800,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/batch": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -5860,18 +5847,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -5972,6 +5947,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001716", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149", "electron-to-chromium": "^1.5.149",
@@ -5995,34 +5971,11 @@
"node-int64": "^0.4.0" "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": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/builtin-modules": { "node_modules/builtin-modules": {
@@ -6469,7 +6422,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@@ -6488,12 +6440,6 @@
"node": ">= 12" "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": { "node_modules/common-tags": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
@@ -6560,35 +6506,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/confusing-browser-globals": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -7327,7 +7244,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -7868,7 +7784,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "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.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -8704,15 +8620,6 @@
"node": ">= 0.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": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -8724,6 +8631,7 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.8.x" "node": ">=0.8.x"
@@ -8875,19 +8783,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -9123,7 +9018,6 @@
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -9779,7 +9673,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -9813,12 +9706,6 @@
"he": "bin/he" "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": { "node_modules/hoopy": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -10198,26 +10085,6 @@
"node": ">=4" "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": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -10345,25 +10212,6 @@
"node": ">= 0.4" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -11112,6 +10960,7 @@
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^27.5.1", "@jest/core": "^27.5.1",
"import-local": "^3.0.2", "import-local": "^3.0.2",
@@ -12065,16 +11914,6 @@
"jiti": "bin/jiti.js" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -12095,12 +11934,6 @@
"js-yaml": "bin/js-yaml.js" "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": { "node_modules/jsdom": {
"version": "16.7.0", "version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -12513,6 +12346,7 @@
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/lz-string": { "node_modules/lz-string": {
@@ -12754,6 +12588,7 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -12782,93 +12617,6 @@
"mkdirp": "bin/cmd.js" "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": { "node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -12961,6 +12709,27 @@
"tslib": "^2.0.3" "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": { "node_modules/node-forge": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "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" "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": { "node_modules/nwsapi": {
"version": "2.2.20", "version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
@@ -13698,6 +13434,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.8", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -14956,6 +14693,7 @@
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssesc": "^3.0.0", "cssesc": "^3.0.0",
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
@@ -15127,19 +14865,11 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "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": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/promise": { "node_modules/promise": {
@@ -15198,6 +14928,12 @@
"node": ">= 0.10" "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": { "node_modules/psl": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "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", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -15463,6 +15200,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -15489,6 +15227,7 @@
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -15577,22 +15316,6 @@
"pify": "^2.3.0" "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": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -15958,12 +15681,6 @@
"node": ">=0.10.0" "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": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -15987,6 +15704,7 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@@ -16242,6 +15960,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -16627,16 +16346,6 @@
"node": ">=8" "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": { "node_modules/sockjs": {
"version": "0.3.24", "version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@@ -16649,18 +16358,14 @@
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
} }
}, },
"node_modules/socks": { "node_modules/sockjs/node_modules/uuid": {
"version": "2.8.4", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "bin": {
"ip-address": "^9.0.5", "uuid": "dist/bin/uuid"
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
} }
}, },
"node_modules/source-list-map": { "node_modules/source-list-map": {
@@ -16851,15 +16556,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -17026,6 +16722,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
@@ -18008,6 +17705,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
@@ -18062,6 +17760,7 @@
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true, "dev": true,
"license": "(MIT OR CC0-1.0)", "license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -18160,12 +17859,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/typedarray-to-buffer": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -18177,9 +17870,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.8.3", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
@@ -18188,7 +17881,7 @@
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
@@ -18221,6 +17914,7 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
@@ -18373,6 +18067,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/util.promisify": { "node_modules/util.promisify": {
@@ -18408,13 +18103,16 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true, "funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
@@ -18528,6 +18226,7 @@
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
@@ -18600,6 +18299,7 @@
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/bonjour": "^3.5.9", "@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5", "@types/connect-history-api-fallback": "^1.3.5",
@@ -19012,6 +18712,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -19268,40 +18969,6 @@
"workbox-core": "6.6.0" "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": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -19363,6 +19030,7 @@
"version": "8.18.2", "version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"axios": "^1.7.7",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@@ -10,9 +11,10 @@
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"mqtt": "^5.3.0", "node-cron": "^3.0.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"uuid": "^11.0.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

348
server.js
View File

@@ -1,126 +1,290 @@
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const cors = require('cors'); const cors = require('cors');
const fs = require('fs');
const bodyParser = require('body-parser'); const sessionStore = require('./services/sessionStore');
const mqtt = require('mqtt'); 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 app = express();
const port = process.env.PORT || 3000; 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 app.use(cors());
function logWithTimestamp(...args) { app.use(express.json({ limit: '1mb' }));
const timestamp = new Date().toISOString(); app.use(express.static(path.join(__dirname, 'build')));
console.log(`[${timestamp}]`, ...args);
function isAdmin(profile) {
if (!adminEmail || !profile?.email) {
return false;
}
return profile.email.toLowerCase() === adminEmail;
} }
function errorWithTimestamp(...args) { function scheduleWithCurrentSettings(sessionId, config) {
const timestamp = new Date().toISOString(); const settings = adminConfig.readSettings();
console.error(`[${timestamp}]`, ...args); scheduleConfig(sessionId, config, settings);
} }
// MQTT-Client mit Authentifizierung initialisieren function rescheduleAllSessions() {
const mqttClient = mqtt.connect(mqttBroker, { const settings = adminConfig.readSettings();
clientId: 'pickup-config-web-' + Math.random().toString(16).substring(2, 8), sessionStore.list().forEach((session) => {
clean: true, if (!session?.profile?.id) {
username: mqttUser, return;
password: mqttPassword }
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;
}
});
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 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' });
}
}); });
// MQTT-Events app.post('/api/auth/logout', requireAuth, (req, res) => {
mqttClient.on('connect', () => { sessionStore.delete(req.session.id);
logWithTimestamp('Verbunden mit MQTT-Broker:', mqttBroker); credentialStore.remove(req.session.profile.id);
res.json({ success: true });
});
mqttClient.subscribe(mqttTopic, (err) => { app.get('/api/auth/session', requireAuth, async (req, res) => {
if (!err) { const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
logWithTimestamp('Abonniert auf Topic:', mqttTopic); let config = readConfig(req.session.profile.id);
mqttClient.publish(mqttTopic + '/get', 'true'); 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
}); });
}); });
mqttClient.on('error', (error) => { app.get('/api/profile', requireAuth, async (req, res) => {
errorWithTimestamp('MQTT-Fehler:', error); const details = await foodsharingClient.fetchProfile(req.session.cookieHeader);
res.json({
profile: details || req.session.profile
});
}); });
mqttClient.on('message', (topic, message) => { app.get('/api/config', requireAuth, (req, res) => {
logWithTimestamp('Nachricht erhalten auf Topic:', topic); const config = readConfig(req.session.profile.id);
res.json(config);
});
if (topic === mqttTopic) { app.post('/api/config', requireAuth, (req, res) => {
try { if (!Array.isArray(req.body)) {
const config = JSON.parse(message.toString()); return res.status(400).json({ error: 'Konfiguration muss ein Array sein' });
logWithTimestamp('Konfiguration vom MQTT-Broker erhalten');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
} catch (error) {
errorWithTimestamp('Fehler beim Verarbeiten der MQTT-Nachricht:', error);
}
} }
writeConfig(req.session.profile.id, req.body);
scheduleWithCurrentSettings(req.session.id, req.body);
res.json({ success: true });
}); });
// Middleware app.get('/api/stores', requireAuth, async (req, res) => {
app.use(cors()); const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
app.use(bodyParser.json()); let config = readConfig(req.session.profile.id);
app.use(express.static(path.join(__dirname, 'build'))); const { merged, changed } = mergeStoresIntoConfig(config, stores);
if (changed) {
// Sicherstellen, dass Konfigurationsordner existiert config = merged;
const configDir = path.dirname(configPath); writeConfig(req.session.profile.id, config);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 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');
}
// 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' });
} }
res.json(stores);
}); });
// API: Konfiguration speichern app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {
app.post('/api/iobroker/pickup-config', (req, res) => { res.json(adminConfig.readSettings());
try { });
const newConfig = req.body;
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); app.post('/api/admin/settings', requireAuth, requireAdmin, (req, res) => {
mqttClient.publish(mqttTopic, JSON.stringify(newConfig)); const updated = adminConfig.writeSettings(req.body || {});
logWithTimestamp('Konfiguration über MQTT gesendet'); rescheduleAllSessions();
res.json({ success: true }); res.json(updated);
} catch (error) { });
errorWithTimestamp('Error saving configuration:', error);
res.status(500).json({ error: 'Failed to save configuration' }); app.get('/api/health', (_req, res) => {
} res.json({ status: 'ok', timestamp: new Date().toISOString() });
}); });
// React-App ausliefern
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html')); res.sendFile(path.join(__dirname, 'build', 'index.html'));
}); });
// Server starten
app.listen(port, () => { 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
View 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
View 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
};

View 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
};

View 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
View 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
View 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();

1063
src/App.js

File diff suppressed because it is too large Load Diff