aktueller Stand
This commit is contained in:
94
services/adminConfig.js
Normal file
94
services/adminConfig.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
const SETTINGS_FILE = path.join(CONFIG_DIR, 'admin-settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
scheduleCron: '*/10 7-22 * * *',
|
||||
randomDelayMinSeconds: 10,
|
||||
randomDelayMaxSeconds: 120,
|
||||
initialDelayMinSeconds: 5,
|
||||
initialDelayMaxSeconds: 30,
|
||||
ignoredSlots: [
|
||||
{
|
||||
storeId: '51450',
|
||||
description: 'TVS'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeNumber(value, fallback) {
|
||||
const num = Number(value);
|
||||
if (Number.isFinite(num) && num >= 0) {
|
||||
return num;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function sanitizeIgnoredSlots(slots = []) {
|
||||
if (!Array.isArray(slots)) {
|
||||
return DEFAULT_SETTINGS.ignoredSlots;
|
||||
}
|
||||
return slots
|
||||
.map((slot) => ({
|
||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||
description: slot?.description ? String(slot.description) : ''
|
||||
}))
|
||||
.filter((slot) => slot.storeId);
|
||||
}
|
||||
|
||||
function readSettings() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(DEFAULT_SETTINGS, null, 2));
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
scheduleCron: parsed.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
|
||||
randomDelayMinSeconds: sanitizeNumber(parsed.randomDelayMinSeconds, DEFAULT_SETTINGS.randomDelayMinSeconds),
|
||||
randomDelayMaxSeconds: sanitizeNumber(parsed.randomDelayMaxSeconds, DEFAULT_SETTINGS.randomDelayMaxSeconds),
|
||||
initialDelayMinSeconds: sanitizeNumber(parsed.initialDelayMinSeconds, DEFAULT_SETTINGS.initialDelayMinSeconds),
|
||||
initialDelayMaxSeconds: sanitizeNumber(parsed.initialDelayMaxSeconds, DEFAULT_SETTINGS.initialDelayMaxSeconds),
|
||||
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Konnte Admin-Einstellungen nicht lesen:', error.message);
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
function writeSettings(patch = {}) {
|
||||
const current = readSettings();
|
||||
const next = {
|
||||
scheduleCron: patch.scheduleCron || current.scheduleCron,
|
||||
randomDelayMinSeconds: sanitizeNumber(patch.randomDelayMinSeconds, current.randomDelayMinSeconds),
|
||||
randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds),
|
||||
initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds),
|
||||
initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds),
|
||||
ignoredSlots:
|
||||
patch.ignoredSlots !== undefined
|
||||
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
||||
: current.ignoredSlots
|
||||
};
|
||||
|
||||
ensureDir();
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2));
|
||||
return next;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SETTINGS,
|
||||
readSettings,
|
||||
writeSettings
|
||||
};
|
||||
48
services/configStore.js
Normal file
48
services/configStore.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const defaultConfig = require('../data/defaultConfig');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigPath(profileId = 'shared') {
|
||||
return path.join(CONFIG_DIR, `${profileId}-pickup-config.json`);
|
||||
}
|
||||
|
||||
function hydrateConfigFile(profileId) {
|
||||
ensureDir();
|
||||
const filePath = getConfigPath(profileId);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function readConfig(profileId) {
|
||||
const filePath = hydrateConfigFile(profileId);
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (err) {
|
||||
console.error(`Failed to read config for ${profileId}:`, err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(profileId, payload) {
|
||||
const filePath = hydrateConfigFile(profileId);
|
||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
||||
return filePath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readConfig,
|
||||
writeConfig,
|
||||
getConfigPath
|
||||
};
|
||||
71
services/credentialStore.js
Normal file
71
services/credentialStore.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CONFIG_DIR = path.join(__dirname, '..', 'config');
|
||||
const CREDENTIAL_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readStore() {
|
||||
ensureDir();
|
||||
if (!fs.existsSync(CREDENTIAL_FILE)) {
|
||||
fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify({}, null, 2));
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(CREDENTIAL_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch (error) {
|
||||
console.error('Konnte Credential-Store nicht lesen:', error.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store) {
|
||||
ensureDir();
|
||||
fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
function save(profileId, credentials) {
|
||||
if (!profileId || !credentials?.email || !credentials?.password) {
|
||||
return;
|
||||
}
|
||||
const store = readStore();
|
||||
store[profileId] = credentials;
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
function remove(profileId) {
|
||||
if (!profileId) {
|
||||
return;
|
||||
}
|
||||
const store = readStore();
|
||||
if (store[profileId]) {
|
||||
delete store[profileId];
|
||||
writeStore(store);
|
||||
}
|
||||
}
|
||||
|
||||
function loadAll() {
|
||||
return readStore();
|
||||
}
|
||||
|
||||
function get(profileId) {
|
||||
if (!profileId) {
|
||||
return null;
|
||||
}
|
||||
const store = readStore();
|
||||
return store[profileId] || null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
save,
|
||||
remove,
|
||||
loadAll,
|
||||
get
|
||||
};
|
||||
179
services/foodsharingClient.js
Normal file
179
services/foodsharingClient.js
Normal file
@@ -0,0 +1,179 @@
|
||||
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 = response.data || [];
|
||||
if (!Array.isArray(stores)) {
|
||||
return [];
|
||||
}
|
||||
return 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 || ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('Stores konnten nicht geladen werden:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
236
services/pickupScheduler.js
Normal file
236
services/pickupScheduler.js
Normal file
@@ -0,0 +1,236 @@
|
||||
const cron = require('node-cron');
|
||||
const foodsharingClient = require('./foodsharingClient');
|
||||
const sessionStore = require('./sessionStore');
|
||||
const { DEFAULT_SETTINGS } = require('./adminConfig');
|
||||
|
||||
const weekdayMap = {
|
||||
Montag: 'Monday',
|
||||
Dienstag: 'Tuesday',
|
||||
Mittwoch: 'Wednesday',
|
||||
Donnerstag: 'Thursday',
|
||||
Freitag: 'Friday',
|
||||
Samstag: 'Saturday',
|
||||
Sonntag: 'Sunday'
|
||||
};
|
||||
|
||||
function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
|
||||
const min = minSeconds * 1000;
|
||||
const max = maxSeconds * 1000;
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function resolveSettings(settings) {
|
||||
if (!settings) {
|
||||
return { ...DEFAULT_SETTINGS };
|
||||
}
|
||||
return {
|
||||
scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
|
||||
randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
|
||||
? settings.randomDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMinSeconds,
|
||||
randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
|
||||
? settings.randomDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.randomDelayMaxSeconds,
|
||||
initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
|
||||
? settings.initialDelayMinSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMinSeconds,
|
||||
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
|
||||
? settings.initialDelayMaxSeconds
|
||||
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
|
||||
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSession(session) {
|
||||
const profileId = session.profile?.id;
|
||||
if (!profileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stillValid = await foodsharingClient.checkSession(session.cookieHeader, profileId);
|
||||
if (stillValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!session.credentials) {
|
||||
console.warn(`Session ${session.id} kann nicht erneuert werden – keine Zugangsdaten gespeichert.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshed = await foodsharingClient.login(
|
||||
session.credentials.email,
|
||||
session.credentials.password
|
||||
);
|
||||
sessionStore.update(session.id, {
|
||||
cookieHeader: refreshed.cookieHeader,
|
||||
csrfToken: refreshed.csrfToken,
|
||||
profile: {
|
||||
...session.profile,
|
||||
...refreshed.profile
|
||||
}
|
||||
});
|
||||
console.log(`Session ${session.id} wurde erfolgreich erneuert.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Session ${session.id} konnte nicht erneuert werden:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesDesiredDate(pickupDate, desiredDate) {
|
||||
if (!desiredDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const desired = new Date(desiredDate);
|
||||
return (
|
||||
pickupDate.getFullYear() === desired.getFullYear() &&
|
||||
pickupDate.getMonth() === desired.getMonth() &&
|
||||
pickupDate.getDate() === desired.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
|
||||
if (!desiredWeekday) {
|
||||
return true;
|
||||
}
|
||||
const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
|
||||
return weekday === desiredWeekday;
|
||||
}
|
||||
|
||||
function shouldIgnoreSlot(entry, pickup, settings) {
|
||||
const rules = settings.ignoredSlots || [];
|
||||
return rules.some((rule) => {
|
||||
if (!rule?.storeId) {
|
||||
return false;
|
||||
}
|
||||
if (String(rule.storeId) !== entry.id) {
|
||||
return false;
|
||||
}
|
||||
if (rule.description) {
|
||||
return pickup.description === rule.description;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function processBooking(session, entry, pickup) {
|
||||
const readableDate = new Date(pickup.date).toLocaleString('de-DE');
|
||||
const storeName = entry.label || entry.id;
|
||||
|
||||
if (entry.onlyNotify) {
|
||||
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const utcDate = new Date(pickup.date).toISOString();
|
||||
try {
|
||||
const allowed = await foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session);
|
||||
if (!allowed) {
|
||||
console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
|
||||
return;
|
||||
}
|
||||
await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
|
||||
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEntry(sessionId, entry, settings) {
|
||||
const session = sessionStore.get(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ready = await ensureSession(session);
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pickups = await foodsharingClient.fetchPickups(entry.id, session.cookieHeader);
|
||||
let hasProfileId = false;
|
||||
let availablePickup = null;
|
||||
|
||||
const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;
|
||||
|
||||
pickups.forEach((pickup) => {
|
||||
const pickupDate = new Date(pickup.date);
|
||||
if (!matchesDesiredDate(pickupDate, entry.desiredDate)) {
|
||||
return;
|
||||
}
|
||||
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
||||
return;
|
||||
}
|
||||
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
|
||||
hasProfileId = true;
|
||||
return;
|
||||
}
|
||||
if (pickup.isAvailable && !availablePickup) {
|
||||
availablePickup = pickup;
|
||||
}
|
||||
});
|
||||
|
||||
if (!availablePickup) {
|
||||
console.log(
|
||||
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
|
||||
hasProfileId ? 'ja' : 'nein'
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldIgnoreSlot(entry, availablePickup, settings)) {
|
||||
console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.checkProfileId || !hasProfileId) {
|
||||
await processBooking(session, entry, availablePickup);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleEntry(sessionId, entry, settings) {
|
||||
const cronExpression = settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron;
|
||||
const job = cron.schedule(
|
||||
cronExpression,
|
||||
() => {
|
||||
const delay = randomDelayMs(
|
||||
settings.randomDelayMinSeconds,
|
||||
settings.randomDelayMaxSeconds
|
||||
);
|
||||
setTimeout(() => checkEntry(sessionId, entry, settings), delay);
|
||||
},
|
||||
{
|
||||
timezone: 'Europe/Berlin'
|
||||
}
|
||||
);
|
||||
sessionStore.attachJob(sessionId, job);
|
||||
setTimeout(
|
||||
() => checkEntry(sessionId, entry, settings),
|
||||
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleConfig(sessionId, config, settings) {
|
||||
const resolvedSettings = resolveSettings(settings);
|
||||
sessionStore.clearJobs(sessionId);
|
||||
const activeEntries = config.filter((entry) => entry.active);
|
||||
if (activeEntries.length === 0) {
|
||||
console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} – Scheduler ruht.`);
|
||||
return;
|
||||
}
|
||||
activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
|
||||
console.log(
|
||||
`[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv (Cron: ${resolvedSettings.scheduleCron}).`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scheduleConfig
|
||||
};
|
||||
98
services/sessionStore.js
Normal file
98
services/sessionStore.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
class SessionStore {
|
||||
constructor() {
|
||||
this.sessions = new Map();
|
||||
this.profileIndex = new Map();
|
||||
}
|
||||
|
||||
create(payload, customId, ttlMs) {
|
||||
const id = customId || uuid();
|
||||
const profileId = payload?.profile?.id ? String(payload.profile.id) : null;
|
||||
|
||||
if (profileId && this.profileIndex.has(profileId)) {
|
||||
const previousId = this.profileIndex.get(profileId);
|
||||
if (previousId && previousId !== id) {
|
||||
this.delete(previousId);
|
||||
}
|
||||
}
|
||||
|
||||
const session = {
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
expiresAt: ttlMs ? Date.now() + ttlMs : null,
|
||||
jobs: [],
|
||||
...payload
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
|
||||
if (profileId) {
|
||||
this.profileIndex.set(profileId, id);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.expiresAt && session.expiresAt < Date.now()) {
|
||||
this.delete(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
update(id, patch) {
|
||||
const session = this.get(id);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
Object.assign(session, patch, { updatedAt: Date.now() });
|
||||
return session;
|
||||
}
|
||||
|
||||
attachJob(id, job) {
|
||||
const session = this.get(id);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
session.jobs.push(job);
|
||||
}
|
||||
|
||||
clearJobs(id) {
|
||||
const session = this.get(id);
|
||||
if (!session || !Array.isArray(session.jobs)) {
|
||||
return;
|
||||
}
|
||||
session.jobs.forEach((job) => {
|
||||
if (job && typeof job.stop === 'function') {
|
||||
job.stop();
|
||||
}
|
||||
});
|
||||
session.jobs = [];
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
const session = this.sessions.get(id);
|
||||
if (session) {
|
||||
this.clearJobs(id);
|
||||
const profileId = session.profile?.id ? String(session.profile.id) : null;
|
||||
if (profileId && this.profileIndex.get(profileId) === id) {
|
||||
this.profileIndex.delete(profileId);
|
||||
}
|
||||
}
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
|
||||
list() {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SessionStore();
|
||||
Reference in New Issue
Block a user