aktueller stand

This commit is contained in:
2026-01-04 00:11:29 +01:00
parent 41ef5107aa
commit 1c12bf6bd1
9 changed files with 1013 additions and 69 deletions

View File

@@ -13,13 +13,15 @@ const client = axios.create({
});
client.interceptors.request.use((config) => {
config.metadata = { startedAt: Date.now() };
const metadata = config.metadata && typeof config.metadata === 'object' ? config.metadata : {};
config.metadata = { ...metadata, startedAt: Date.now() };
return config;
});
client.interceptors.response.use(
(response) => {
const startedAt = response?.config?.metadata?.startedAt || Date.now();
const metadata = response?.config?.metadata || {};
try {
requestLogStore.add({
direction: 'outgoing',
@@ -28,6 +30,9 @@ client.interceptors.response.use(
path: response.config?.url || '',
status: response.status,
durationMs: Date.now() - startedAt,
sessionId: metadata.sessionId ?? null,
profileId: metadata.profileId ?? null,
profileName: metadata.profileName ?? null,
responseBody: response.data
});
} catch (error) {
@@ -37,6 +42,7 @@ client.interceptors.response.use(
},
(error) => {
const startedAt = error?.config?.metadata?.startedAt || Date.now();
const metadata = error?.config?.metadata || {};
try {
requestLogStore.add({
direction: 'outgoing',
@@ -45,6 +51,9 @@ client.interceptors.response.use(
path: error.config?.url || '',
status: error?.response?.status || null,
durationMs: Date.now() - startedAt,
sessionId: metadata.sessionId ?? null,
profileId: metadata.profileId ?? null,
profileName: metadata.profileName ?? null,
error: error?.message || 'Unbekannter Fehler',
responseBody: error?.response?.data
});
@@ -55,7 +64,7 @@ client.interceptors.response.use(
}
);
const CSRF_COOKIE_NAMES = ['CSRF_TOKEN', 'CSRF-TOKEN', 'XSRF-TOKEN', 'XSRF_TOKEN'];
const CSRF_COOKIE_NAMES = ['FS_CSRF_TOKEN', 'CSRF_TOKEN', 'CSRF-TOKEN', 'XSRF-TOKEN', 'XSRF_TOKEN'];
function extractCookieValue(cookies = [], name) {
if (!Array.isArray(cookies) || !name) {
@@ -114,10 +123,46 @@ function buildHeaders(cookieHeader, csrfToken) {
return headers;
}
async function getCurrentUserDetails(cookieHeader) {
const response = await client.get('/api/user/current/details', {
headers: buildHeaders(cookieHeader)
});
function buildRequestMetadata(context) {
if (!context) {
return {};
}
if (context.sessionId || context.profileId) {
return {
sessionId: context.sessionId ?? null,
profileId: context.profileId ?? null,
profileName: context.profileName ?? null
};
}
if (context.id || context.profile?.id) {
return {
sessionId: context.id ?? null,
profileId: context.profile?.id ?? null,
profileName: context.profile?.name ?? null
};
}
return {};
}
function buildRequestConfig({ cookieHeader, csrfToken, context, params } = {}) {
const metadata = buildRequestMetadata(context);
const config = {
headers: buildHeaders(cookieHeader, csrfToken)
};
if (Object.keys(metadata).length > 0) {
config.metadata = metadata;
}
if (params) {
config.params = params;
}
return config;
}
async function getCurrentUserDetails(cookieHeader, context) {
const response = await client.get(
'/api/user/current/details',
buildRequestConfig({ cookieHeader, context })
);
return response.data;
}
@@ -165,9 +210,9 @@ async function login(email, password) {
};
}
async function fetchProfile(cookieHeader, { throwOnError = false } = {}) {
async function fetchProfile(cookieHeader, { throwOnError = false } = {}, context) {
try {
return await getCurrentUserDetails(cookieHeader);
return await getCurrentUserDetails(cookieHeader, context);
} catch (error) {
if (throwOnError) {
throw error;
@@ -181,7 +226,7 @@ function wait(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchStores(cookieHeader, profileId, options = {}) {
async function fetchStores(cookieHeader, profileId, options = {}, context) {
if (!profileId) {
return [];
}
@@ -193,10 +238,10 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
? options.onStoreCheck
: null;
try {
const response = await client.get(`/api/user/${profileId}/stores`, {
headers: buildHeaders(cookieHeader),
params: { activeStores: 1 }
});
const response = await client.get(
`/api/user/${profileId}/stores`,
buildRequestConfig({ cookieHeader, params: { activeStores: 1 }, context })
);
const stores = Array.isArray(response.data) ? response.data : [];
const normalized = stores.map((store) => ({
id: String(store.id),
@@ -209,14 +254,26 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
zip: store.zip || ''
}));
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck);
return annotateStoresWithPickupSlots(
normalized,
cookieHeader,
delayBetweenRequestsMs,
onStoreCheck,
context
);
} catch (error) {
console.warn('Stores konnten nicht geladen werden:', error.message);
return [];
}
}
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0, onStoreCheck) {
async function annotateStoresWithPickupSlots(
stores,
cookieHeader,
delayBetweenRequestsMs = 0,
onStoreCheck,
context
) {
if (!Array.isArray(stores) || stores.length === 0) {
return [];
}
@@ -238,7 +295,7 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
}
let hasPickupSlots = null;
try {
const pickups = await fetchPickups(store.id, cookieHeader);
const pickups = await fetchPickups(store.id, cookieHeader, context);
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
} catch (error) {
const status = error?.response?.status;
@@ -255,50 +312,59 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
return annotated;
}
async function fetchPickups(storeId, cookieHeader) {
const response = await client.get(`/api/stores/${storeId}/pickups`, {
headers: buildHeaders(cookieHeader)
});
async function fetchPickups(storeId, cookieHeader, context) {
const response = await client.get(
`/api/stores/${storeId}/pickups`,
buildRequestConfig({ cookieHeader, context })
);
return response.data?.pickups || [];
}
async function fetchRegionStores(regionId, cookieHeader) {
async function fetchRegionStores(regionId, cookieHeader, context) {
if (!regionId) {
return { total: 0, stores: [] };
}
const response = await client.get(`/api/region/${regionId}/stores`, {
headers: buildHeaders(cookieHeader)
});
const response = await client.get(
`/api/region/${regionId}/stores`,
buildRequestConfig({ cookieHeader, context })
);
return {
total: Number(response.data?.total) || 0,
stores: Array.isArray(response.data?.stores) ? response.data.stores : []
};
}
async function fetchStoreDetails(storeId, cookieHeader) {
async function fetchStoreDetails(storeId, cookieHeader, context) {
if (!storeId) {
return null;
}
const response = await client.get(`/api/map/stores/${storeId}`, {
headers: buildHeaders(cookieHeader)
});
const response = await client.get(
`/api/map/stores/${storeId}`,
buildRequestConfig({ cookieHeader, context })
);
return response.data || null;
}
async function pickupRuleCheck(storeId, utcDate, profileId, session) {
const response = await client.get(`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, {
headers: buildHeaders(session.cookieHeader, session.csrfToken)
});
const response = await client.get(
`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`,
buildRequestConfig({
cookieHeader: session.cookieHeader,
csrfToken: session.csrfToken,
context: session
})
);
return response.data?.result === true;
}
async function fetchStoreMembers(storeId, cookieHeader) {
async function fetchStoreMembers(storeId, cookieHeader, context) {
if (!storeId) {
return [];
}
const response = await client.get(`/api/stores/${storeId}/member`, {
headers: buildHeaders(cookieHeader)
});
const response = await client.get(
`/api/stores/${storeId}/member`,
buildRequestConfig({ cookieHeader, context })
);
return Array.isArray(response.data) ? response.data : [];
}
@@ -307,7 +373,11 @@ async function bookSlot(storeId, utcDate, profileId, session) {
`/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
{},
{
headers: buildHeaders(session.cookieHeader, session.csrfToken)
...buildRequestConfig({
cookieHeader: session.cookieHeader,
csrfToken: session.csrfToken,
context: session
})
}
);
}

View File

@@ -357,7 +357,7 @@ async function checkEntry(sessionId, entry, settings) {
try {
const pickups = await withSessionRetry(
session,
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader),
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
{ label: 'fetchPickups' }
);
let hasProfileId = false;
@@ -428,7 +428,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
try {
const details = await withSessionRetry(
session,
() => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader),
() => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' }
);
const status = details?.teamSearchStatus === 1 ? 1 : 0;
@@ -578,6 +578,19 @@ async function runStoreWatchCheck(sessionId, settings, options = {}) {
return checkWatchedStores(sessionId, resolvedSettings, options);
}
async function runImmediatePickupCheck(sessionId, config, settings) {
const resolvedSettings = resolveSettings(settings);
const entries = Array.isArray(config) ? config : [];
const activeEntries = entries.filter((entry) => entry?.active);
if (activeEntries.length === 0) {
return { checked: 0 };
}
for (const entry of activeEntries) {
await checkEntry(sessionId, entry, resolvedSettings);
}
return { checked: activeEntries.length };
}
function setMonthOffset(date, offset) {
const copy = new Date(date.getTime());
copy.setMonth(copy.getMonth() + offset);
@@ -593,11 +606,14 @@ function getMissingLastPickupStoreIds(config = []) {
.map((entry) => String(entry.id));
}
async function checkDormantMembers(sessionId) {
async function checkDormantMembers(sessionId, options = {}) {
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return;
}
const storeIdSet = Array.isArray(options.storeIds)
? new Set(options.storeIds.map((storeId) => String(storeId)))
: null;
const profileId = session.profile.id;
const ensured = await ensureSession(session);
if (!ensured) {
@@ -615,26 +631,54 @@ async function checkDormantMembers(sessionId) {
});
let configChanged = false;
const storeTargets = new Map();
config.forEach((entry) => {
if (!entry?.id || entry.hidden) {
return;
}
const storeId = String(entry.id);
if (storeIdSet && !storeIdSet.has(storeId)) {
return;
}
if (skipMap.get(storeId)) {
return;
}
storeTargets.set(storeId, {
storeId,
storeName: entry.label || `Store ${storeId}`
});
});
const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : [];
if (stores.length === 0) {
console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`);
} else {
stores.forEach((store) => {
const storeId = store?.id ? String(store.id) : null;
if (!storeId || !storeTargets.has(storeId)) {
return;
}
const target = storeTargets.get(storeId);
storeTargets.set(storeId, {
...target,
storeName: store.name || target.storeName
});
});
}
if (storeTargets.size === 0) {
return;
}
const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
for (const store of stores) {
const storeId = store?.id ? String(store.id) : null;
if (!storeId) {
continue;
}
if (skipMap.get(storeId)) {
continue;
}
for (const target of storeTargets.values()) {
const storeId = target.storeId;
let members = [];
try {
members = await withSessionRetry(
session,
() => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader),
() => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader, session),
{ label: 'fetchStoreMembers' }
);
} catch (error) {
@@ -671,7 +715,7 @@ async function checkDormantMembers(sessionId) {
try {
await sendDormantPickupWarning({
profileId,
storeName: store.name || `Store ${storeId}`,
storeName: target.storeName,
storeId,
reasonLines: reasons
});
@@ -722,7 +766,13 @@ function scheduleDormantMembershipCheck(sessionId) {
setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
}
async function runDormantMembershipCheck(sessionId, options = {}) {
await checkDormantMembers(sessionId, options);
}
module.exports = {
scheduleConfig,
runStoreWatchCheck
runStoreWatchCheck,
runImmediatePickupCheck,
runDormantMembershipCheck
};