Files
Pickup-Config/services/foodsharingClient.js

309 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const axios = require('axios');
const requestLogStore = require('./requestLogStore');
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, */*'
}
});
client.interceptors.request.use((config) => {
config.metadata = { startedAt: Date.now() };
return config;
});
client.interceptors.response.use(
(response) => {
const startedAt = response?.config?.metadata?.startedAt || Date.now();
try {
requestLogStore.add({
direction: 'outgoing',
target: 'foodsharing.de',
method: (response.config?.method || 'GET').toUpperCase(),
path: response.config?.url || '',
status: response.status,
durationMs: Date.now() - startedAt,
responseBody: response.data
});
} catch (error) {
console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message);
}
return response;
},
(error) => {
const startedAt = error?.config?.metadata?.startedAt || Date.now();
try {
requestLogStore.add({
direction: 'outgoing',
target: 'foodsharing.de',
method: (error.config?.method || 'GET').toUpperCase(),
path: error.config?.url || '',
status: error?.response?.status || null,
durationMs: Date.now() - startedAt,
error: error?.message || 'Unbekannter Fehler',
responseBody: error?.response?.data
});
} catch (logError) {
console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message);
}
return Promise.reject(error);
}
);
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 fetchRegionStores(regionId, cookieHeader) {
if (!regionId) {
return { total: 0, stores: [] };
}
const response = await client.get(`/api/region/${regionId}/stores`, {
headers: buildHeaders(cookieHeader)
});
return {
total: Number(response.data?.total) || 0,
stores: Array.isArray(response.data?.stores) ? response.data.stores : []
};
}
async function fetchStoreDetails(storeId, cookieHeader) {
if (!storeId) {
return null;
}
const response = await client.get(`/api/map/stores/${storeId}`, {
headers: buildHeaders(cookieHeader)
});
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)
});
return response.data?.result === true;
}
async function fetchStoreMembers(storeId, cookieHeader) {
if (!storeId) {
return [];
}
const response = await client.get(`/api/stores/${storeId}/member`, {
headers: buildHeaders(cookieHeader)
});
return Array.isArray(response.data) ? response.data : [];
}
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,
fetchRegionStores,
fetchStoreDetails,
fetchStoreMembers,
pickupRuleCheck,
bookSlot
};