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

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