210 lines
5.4 KiB
JavaScript
210 lines
5.4 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|
|
async function fetchStores(cookieHeader, profileId) {
|
|
if (!profileId) {
|
|
return [];
|
|
}
|
|
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);
|
|
} catch (error) {
|
|
console.warn('Stores konnten nicht geladen werden:', error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function annotateStoresWithPickupSlots(stores, cookieHeader, concurrency = 5) {
|
|
if (!Array.isArray(stores) || stores.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const cappedConcurrency = Math.max(1, Math.min(concurrency, stores.length));
|
|
const results = new Array(stores.length);
|
|
let pointer = 0;
|
|
|
|
async function worker() {
|
|
while (true) {
|
|
const currentIndex = pointer++;
|
|
if (currentIndex >= stores.length) {
|
|
return;
|
|
}
|
|
const store = stores[currentIndex];
|
|
let hasPickupSlots = null;
|
|
try {
|
|
const pickups = await fetchPickups(store.id, cookieHeader);
|
|
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
|
|
} catch (error) {
|
|
console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message);
|
|
}
|
|
results[currentIndex] = { ...store, hasPickupSlots };
|
|
}
|
|
}
|
|
|
|
await Promise.all(Array.from({ length: cappedConcurrency }, () => worker()));
|
|
return results;
|
|
}
|
|
|
|
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
|
|
};
|