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; } } function wait(ms = 0) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fetchStores(cookieHeader, profileId, options = {}) { if (!profileId) { return []; } const delayBetweenRequestsMs = Number.isFinite(options.delayBetweenRequestsMs) ? Math.max(0, options.delayBetweenRequestsMs) : 0; const onStoreCheck = typeof options.onStoreCheck === 'function' ? options.onStoreCheck : null; try { const response = await client.get(`/api/user/${profileId}/stores`, { headers: buildHeaders(cookieHeader), params: { activeStores: 1 } }); const stores = Array.isArray(response.data) ? response.data : []; const normalized = 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 || '' })); return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck); } catch (error) { console.warn('Stores konnten nicht geladen werden:', error.message); return []; } } async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0, onStoreCheck) { if (!Array.isArray(stores) || stores.length === 0) { return []; } const delayMs = Number.isFinite(delayBetweenRequestsMs) ? Math.max(0, delayBetweenRequestsMs) : 0; const annotated = []; for (let index = 0; index < stores.length; index += 1) { const store = stores[index]; if (onStoreCheck) { try { onStoreCheck(store, index + 1, stores.length); } catch (callbackError) { console.warn('Store-Progress-Callback fehlgeschlagen:', callbackError); } } if (delayMs > 0 && index > 0) { await wait(delayMs); } let hasPickupSlots = null; try { const pickups = await fetchPickups(store.id, cookieHeader); hasPickupSlots = Array.isArray(pickups) && pickups.length > 0; } catch (error) { const status = error?.response?.status; if (status === 403) { hasPickupSlots = false; console.warn(`Pickups für Store ${store.id} nicht erlaubt (403) – wird als ohne Slots behandelt.`); } else { console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message); } } annotated.push({ ...store, hasPickupSlots }); } return annotated; } 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 };