aktueller stand

This commit is contained in:
2026-01-29 17:50:31 +01:00
parent 916ca1dbc2
commit 9f2825edd4
4 changed files with 4794 additions and 20 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
const axios = require('axios'); const axios = require('axios');
const requestLogStore = require('./requestLogStore'); const requestLogStore = require('./requestLogStore');
const sessionStore = require('./sessionStore');
const BASE_URL = 'https://foodsharing.de'; const BASE_URL = 'https://foodsharing.de';
@@ -38,6 +39,7 @@ client.interceptors.response.use(
} catch (error) { } catch (error) {
console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message); console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message);
} }
updateSessionCookiesFromResponse(response);
return response; return response;
}, },
(error) => { (error) => {
@@ -60,6 +62,9 @@ client.interceptors.response.use(
} catch (logError) { } catch (logError) {
console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message); console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message);
} }
if (error?.response) {
updateSessionCookiesFromResponse(error.response);
}
return Promise.reject(error); return Promise.reject(error);
} }
); );
@@ -158,12 +163,75 @@ function buildRequestConfig({ cookieHeader, csrfToken, context, params } = {}) {
return config; return config;
} }
async function getCurrentUserDetails(cookieHeader, context) { function mergeCookieHeaders(existingHeader = '', setCookies = []) {
const cookieMap = new Map();
if (existingHeader) {
existingHeader.split(';').forEach((part) => {
const trimmed = part.trim();
if (!trimmed) {
return;
}
const [name, ...valueParts] = trimmed.split('=');
if (!name) {
return;
}
cookieMap.set(name, valueParts.join('='));
});
}
if (Array.isArray(setCookies)) {
setCookies.forEach((cookie) => {
if (!cookie || typeof cookie !== 'string') {
return;
}
const [nameValue] = cookie.split(';');
if (!nameValue) {
return;
}
const [name, ...valueParts] = nameValue.split('=');
if (!name) {
return;
}
cookieMap.set(name, valueParts.join('='));
});
}
return Array.from(cookieMap.entries())
.map(([name, value]) => `${name}=${value}`)
.join('; ');
}
function updateSessionCookiesFromResponse(response) {
const setCookies = response?.headers?.['set-cookie'];
if (!Array.isArray(setCookies) || setCookies.length === 0) {
return;
}
const metadata = response?.config?.metadata || {};
const sessionId = metadata.sessionId;
if (!sessionId) {
return;
}
const session = sessionStore.get(sessionId);
if (!session) {
return;
}
const mergedCookieHeader = mergeCookieHeaders(session.cookieHeader, setCookies);
if (!mergedCookieHeader) {
return;
}
const csrfToken = extractCsrfTokenFromCookieHeader(mergedCookieHeader) || session.csrfToken || null;
sessionStore.update(sessionId, {
cookieHeader: mergedCookieHeader,
csrfToken
});
}
async function getCurrentUserDetails(cookieHeader, context, options = {}) {
const response = await client.get( const response = await client.get(
'/api/user/current/details', '/api/user/current/details',
buildRequestConfig({ cookieHeader, context }) buildRequestConfig({ cookieHeader, context })
); );
return response.data; return options.raw ? response : response.data;
} }
async function login(email, password) { async function login(email, password) {
@@ -185,8 +253,14 @@ async function login(email, password) {
const response = await client.post('/api/user/login', payload, { headers }); const response = await client.post('/api/user/login', payload, { headers });
const cookies = response.headers['set-cookie'] || []; const cookies = response.headers['set-cookie'] || [];
const csrfToken = extractCsrfToken(cookies); const csrfToken = extractCsrfToken(cookies);
const cookieHeader = serializeCookies(cookies); let cookieHeader = serializeCookies(cookies);
const details = await getCurrentUserDetails(cookieHeader); const detailsResponse = await getCurrentUserDetails(cookieHeader, null, { raw: true });
const detailsCookies = detailsResponse?.headers?.['set-cookie'] || [];
if (detailsCookies.length > 0) {
cookieHeader = mergeCookieHeaders(cookieHeader, detailsCookies);
}
const updatedCsrf = extractCsrfTokenFromCookieHeader(cookieHeader) || csrfToken;
const details = detailsResponse?.data;
if (!details?.id) { if (!details?.id) {
throw new Error('Profil-ID konnte nicht ermittelt werden.'); throw new Error('Profil-ID konnte nicht ermittelt werden.');
} }
@@ -200,7 +274,7 @@ async function login(email, password) {
} }
return { return {
csrfToken, csrfToken: updatedCsrf,
cookieHeader, cookieHeader,
profile: { profile: {
id: String(details.id), id: String(details.id),

View File

@@ -19,6 +19,9 @@ function wait(ms) {
const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120; const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120;
const storeWatchInFlight = new Map(); const storeWatchInFlight = new Map();
const pickupCheckInFlight = new Map();
const pickupCheckLastRun = new Map();
const PICKUP_CHECK_DEDUP_MS = 30 * 1000;
async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) { async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) {
if (!storeId) { if (!storeId) {
@@ -216,6 +219,15 @@ function persistEntryDeactivation(profileId, entryId, options = {}) {
} }
} }
function isEntryActiveInConfig(profileId, entryId) {
if (!profileId || !entryId) {
return false;
}
const config = readConfig(profileId);
const entry = config.find((item) => String(item?.id) === String(entryId));
return !!entry && entry.active !== false;
}
function toDateValue(input) { function toDateValue(input) {
if (!input) { if (!input) {
return null; return null;
@@ -434,18 +446,34 @@ async function checkEntry(sessionId, entry, settings) {
if (!session) { if (!session) {
return; return;
} }
const dedupKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
if (desiredWindowExpired(entry)) { const lastRun = pickupCheckLastRun.get(dedupKey);
await handleExpiredDesiredWindow(session, entry); if (lastRun && Date.now() - lastRun < PICKUP_CHECK_DEDUP_MS) {
return; return;
} }
pickupCheckLastRun.set(dedupKey, Date.now());
const ready = await ensureSession(session); const inFlightKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
if (!ready) { if (pickupCheckInFlight.has(inFlightKey)) {
return; return;
} }
pickupCheckInFlight.set(inFlightKey, Date.now());
try { try {
if (desiredWindowExpired(entry)) {
await handleExpiredDesiredWindow(session, entry);
return;
}
if (!isEntryActiveInConfig(session.profile.id, entry.id)) {
deactivateEntryInMemory(entry);
return;
}
const ready = await ensureSession(session);
if (!ready) {
return;
}
const pickups = await withSessionRetry( const pickups = await withSessionRetry(
session, session,
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session), () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
@@ -458,21 +486,30 @@ async function checkEntry(sessionId, entry, settings) {
pickups.forEach((pickup) => { pickups.forEach((pickup) => {
const pickupDate = new Date(pickup.date); const pickupDate = new Date(pickup.date);
if (
entry.checkProfileId &&
pickup.occupiedSlots?.some((slot) => String(slot.profile?.id) === String(session.profile.id))
) {
hasProfileId = true;
}
if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) { if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
return; return;
} }
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) { if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
return; return;
} }
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
hasProfileId = true;
return;
}
if (pickup.isAvailable && !availablePickup) { if (pickup.isAvailable && !availablePickup) {
availablePickup = pickup; availablePickup = pickup;
} }
}); });
if (entry.checkProfileId && hasProfileId) {
console.log(
`[INFO] Profil bereits in einem Slot für ${entry.label || entry.id} eingetragen überspringe Buchung.`
);
return;
}
if (!availablePickup) { if (!availablePickup) {
console.log( console.log(
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${ `[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
@@ -492,6 +529,8 @@ async function checkEntry(sessionId, entry, settings) {
} }
} catch (error) { } catch (error) {
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message); console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
} finally {
pickupCheckInFlight.delete(inFlightKey);
} }
} }
@@ -637,10 +676,6 @@ function scheduleEntry(sessionId, entry, settings) {
} }
); );
sessionStore.attachJob(sessionId, job); sessionStore.attachJob(sessionId, job);
setTimeout(
() => checkEntry(sessionId, entry, settings),
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
);
} }
function scheduleConfig(sessionId, config, settings) { function scheduleConfig(sessionId, config, settings) {

View File

@@ -6,6 +6,21 @@ function isUnauthorizedError(error) {
return status === 401 || status === 403; return status === 401 || status === 403;
} }
function isCsrfError(error) {
const status = error?.response?.status;
if (status !== 400) {
return false;
}
const data = error?.response?.data;
const message =
typeof data === 'string'
? data
: typeof data?.message === 'string'
? data.message
: '';
return message.toLowerCase().includes('csrf');
}
async function refreshSession(session, { label } = {}) { async function refreshSession(session, { label } = {}) {
if (!session?.credentials?.email || !session?.credentials?.password) { if (!session?.credentials?.email || !session?.credentials?.password) {
console.warn( console.warn(
@@ -62,7 +77,7 @@ async function withSessionRetry(session, action, { label } = {}) {
try { try {
return await action(); return await action();
} catch (error) { } catch (error) {
if (!isUnauthorizedError(error)) { if (!isUnauthorizedError(error) && !isCsrfError(error)) {
throw error; throw error;
} }
const refreshed = await refreshSession(session, { label }); const refreshed = await refreshSession(session, { label });