aktueller Stand
This commit is contained in:
236
services/pickupScheduler.js
Normal file
236
services/pickupScheduler.js
Normal file
@@ -0,0 +1,236 @@
|
||||
const cron = require('node-cron');
|
||||
const foodsharingClient = require('./foodsharingClient');
|
||||
const sessionStore = require('./sessionStore');
|
||||
const { DEFAULT_SETTINGS } = require('./adminConfig');
|
||||
|
||||
const weekdayMap = {
|
||||
Montag: 'Monday',
|
||||
Dienstag: 'Tuesday',
|
||||
Mittwoch: 'Wednesday',
|
||||
Donnerstag: 'Thursday',
|
||||
Freitag: 'Friday',
|
||||
Samstag: 'Saturday',
|
||||
Sonntag: 'Sunday'
|
||||
};
|
||||
|
||||
function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
|
||||
const min = minSeconds * 1000;
|
||||
const max = maxSeconds * 1000;
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function resolveSettings(settings) {
|
||||
if (!settings) {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
return {
|
||||
scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
|
||||
randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
|
||||
? settings.randomDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMinSeconds,
|
||||
randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
|
||||
? settings.randomDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMaxSeconds,
|
||||
initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
|
||||
? settings.initialDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMinSeconds,
|
||||
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
|
||||
? settings.initialDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
|
||||
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSession(session) {
|
||||
const profileId = session.profile?.id;
|
||||
if (!profileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stillValid = await foodsharingClient.checkSession(session.cookieHeader, profileId);
|
||||
if (stillValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!session.credentials) {
|
||||
console.warn(`Session ${session.id} kann nicht erneuert werden – keine Zugangsdaten gespeichert.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshed = await foodsharingClient.login(
|
||||
session.credentials.email,
|
||||
session.credentials.password
|
||||
);
|
||||
sessionStore.update(session.id, {
|
||||
cookieHeader: refreshed.cookieHeader,
|
||||
csrfToken: refreshed.csrfToken,
|
||||
profile: {
|
||||
...session.profile,
|
||||
...refreshed.profile
|
||||
}
|
||||
});
|
||||
console.log(`Session ${session.id} wurde erfolgreich erneuert.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Session ${session.id} konnte nicht erneuert werden:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesDesiredDate(pickupDate, desiredDate) {
|
||||
if (!desiredDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const desired = new Date(desiredDate);
|
||||
return (
|
||||
pickupDate.getFullYear() === desired.getFullYear() &&
|
||||
pickupDate.getMonth() === desired.getMonth() &&
|
||||
pickupDate.getDate() === desired.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
|
||||
if (!desiredWeekday) {
|
||||
return true;
|
||||
}
|
||||
const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
|
||||
return weekday === desiredWeekday;
|
||||
}
|
||||
|
||||
function shouldIgnoreSlot(entry, pickup, settings) {
|
||||
const rules = settings.ignoredSlots || [];
|
||||
return rules.some((rule) => {
|
||||
if (!rule?.storeId) {
|
||||
return false;
|
||||
}
|
||||
if (String(rule.storeId) !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
if (rule.description) {
|
||||
return pickup.description === rule.description;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function processBooking(session, entry, pickup) {
|
||||
const readableDate = new Date(pickup.date).toLocaleString('de-DE');
|
||||
const storeName = entry.label || entry.id;
|
||||
|
||||
if (entry.onlyNotify) {
|
||||
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const utcDate = new Date(pickup.date).toISOString();
|
||||
try {
|
||||
const allowed = await foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session);
|
||||
if (!allowed) {
|
||||
console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
|
||||
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEntry(sessionId, entry, settings) {
|
||||
const session = sessionStore.get(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ready = await ensureSession(session);
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pickups = await foodsharingClient.fetchPickups(entry.id, session.cookieHeader);
|
||||
let hasProfileId = false;
|
||||
let availablePickup = null;
|
||||
|
||||
const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;
|
||||
|
||||
pickups.forEach((pickup) => {
|
||||
const pickupDate = new Date(pickup.date);
|
||||
if (!matchesDesiredDate(pickupDate, entry.desiredDate)) {
|
||||
return;
|
||||
}
|
||||
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
||||
return;
|
||||
}
|
||||
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
|
||||
hasProfileId = true;
|
||||
return;
|
||||
}
|
||||
if (pickup.isAvailable && !availablePickup) {
|
||||
availablePickup = pickup;
|
||||
}
|
||||
});
|
||||
|
||||
if (!availablePickup) {
|
||||
console.log(
|
||||
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
|
||||
hasProfileId ? 'ja' : 'nein'
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldIgnoreSlot(entry, availablePickup, settings)) {
|
||||
console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.checkProfileId || !hasProfileId) {
|
||||
await processBooking(session, entry, availablePickup);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleEntry(sessionId, entry, settings) {
|
||||
const cronExpression = settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron;
|
||||
const job = cron.schedule(
|
||||
cronExpression,
|
||||
() => {
|
||||
const delay = randomDelayMs(
|
||||
settings.randomDelayMinSeconds,
|
||||
settings.randomDelayMaxSeconds
|
||||
);
|
||||
setTimeout(() => checkEntry(sessionId, entry, settings), delay);
|
||||
},
|
||||
{
|
||||
timezone: 'Europe/Berlin'
|
||||
}
|
||||
);
|
||||
sessionStore.attachJob(sessionId, job);
|
||||
setTimeout(
|
||||
() => checkEntry(sessionId, entry, settings),
|
||||
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleConfig(sessionId, config, settings) {
|
||||
const resolvedSettings = resolveSettings(settings);
|
||||
sessionStore.clearJobs(sessionId);
|
||||
const activeEntries = config.filter((entry) => entry.active);
|
||||
if (activeEntries.length === 0) {
|
||||
console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} – Scheduler ruht.`);
|
||||
return;
|
||||
}
|
||||
activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
|
||||
console.log(
|
||||
`[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv (Cron: ${resolvedSettings.scheduleCron}).`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scheduleConfig
|
||||
};
|
||||
Reference in New Issue
Block a user