365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
const axios = require('axios');
|
||
const adminConfig = require('./adminConfig');
|
||
const { readNotificationSettings } = require('./userSettingsStore');
|
||
const DISPLAY_TIME_ZONE = 'Europe/Berlin';
|
||
|
||
function formatDateLabel(dateInput) {
|
||
try {
|
||
const date = new Date(dateInput);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return 'unbekanntes Datum';
|
||
}
|
||
return date.toLocaleString('de-DE', {
|
||
weekday: 'short',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
timeZone: DISPLAY_TIME_ZONE
|
||
});
|
||
} catch (_error) {
|
||
return 'unbekanntes Datum';
|
||
}
|
||
}
|
||
|
||
function formatDateOnly(dateInput) {
|
||
try {
|
||
const date = new Date(dateInput);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return 'unbekanntes Datum';
|
||
}
|
||
return date.toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
timeZone: DISPLAY_TIME_ZONE
|
||
});
|
||
} catch (_error) {
|
||
return 'unbekanntes Datum';
|
||
}
|
||
}
|
||
|
||
function extractFirstName(profileName) {
|
||
if (!profileName || typeof profileName !== 'string') {
|
||
return null;
|
||
}
|
||
const trimmed = profileName.trim();
|
||
if (!trimmed) {
|
||
return null;
|
||
}
|
||
if (trimmed.includes(',')) {
|
||
const parts = trimmed.split(',').map((part) => part.trim()).filter(Boolean);
|
||
if (parts.length > 1) {
|
||
return parts[1].split(/\s+/)[0] || null;
|
||
}
|
||
}
|
||
return trimmed.split(/\s+/)[0] || null;
|
||
}
|
||
|
||
async function sendNtfyNotification(adminNtfy, userNtfy, payload) {
|
||
if (!adminNtfy?.enabled || !userNtfy?.enabled || !userNtfy.topic) {
|
||
return;
|
||
}
|
||
const server = (userNtfy.serverUrl || adminNtfy.serverUrl || 'https://ntfy.sh').replace(/\/+$/, '');
|
||
const sanitizedPrefix = (adminNtfy.topicPrefix || '').replace(/^-+|-+$/g, '');
|
||
const sanitizedTopic = (userNtfy.topic || '').replace(/^-+|-+$/g, '');
|
||
const topic = `${sanitizedPrefix}${sanitizedTopic}` || sanitizedPrefix || sanitizedTopic;
|
||
let url = `${server}/${topic}`;
|
||
if (payload.title) {
|
||
const delimiter = url.includes('?') ? '&' : '?';
|
||
url = `${url}${delimiter}title=${encodeURIComponent(payload.title)}`;
|
||
}
|
||
const headers = {
|
||
Priority: payload.priority || 'default',
|
||
'Content-Type': 'text/plain; charset=utf-8'
|
||
};
|
||
if (adminNtfy.username && adminNtfy.password) {
|
||
const credentials = Buffer.from(`${adminNtfy.username}:${adminNtfy.password}`).toString('base64');
|
||
headers.Authorization = `Basic ${credentials}`;
|
||
}
|
||
await axios.post(url, payload.message, { headers, timeout: 15000 });
|
||
}
|
||
|
||
async function sendTelegramNotification(adminTelegram, userTelegram, payload) {
|
||
if (!adminTelegram?.enabled || !adminTelegram.botToken || !userTelegram?.enabled || !userTelegram.chatId) {
|
||
return;
|
||
}
|
||
const endpoint = `https://api.telegram.org/bot${adminTelegram.botToken}/sendMessage`;
|
||
const profileLabel = extractFirstName(payload?.profileName);
|
||
const messageParts = [
|
||
payload?.title ? `*${payload.title}*` : null,
|
||
profileLabel ? `Profil: ${profileLabel}` : null,
|
||
payload?.message || null
|
||
].filter(Boolean);
|
||
await axios.post(
|
||
endpoint,
|
||
{
|
||
chat_id: userTelegram.chatId,
|
||
text: messageParts.join('\n'),
|
||
parse_mode: payload?.title ? 'Markdown' : undefined,
|
||
disable_web_page_preview: true
|
||
},
|
||
{ timeout: 15000 }
|
||
);
|
||
}
|
||
|
||
async function sendAdminTelegramNotification(payload) {
|
||
const adminSettings = adminConfig.readSettings();
|
||
const adminTelegram = adminSettings.notifications?.telegram;
|
||
if (!adminTelegram?.enabled || !adminTelegram.botToken || !adminTelegram.chatId) {
|
||
return;
|
||
}
|
||
await sendTelegramNotification(
|
||
adminTelegram,
|
||
{ enabled: true, chatId: adminTelegram.chatId },
|
||
payload
|
||
);
|
||
}
|
||
|
||
async function notifyChannels(profileId, template) {
|
||
const adminSettings = adminConfig.readSettings();
|
||
const userSettings = readNotificationSettings(profileId);
|
||
try {
|
||
await Promise.allSettled([
|
||
sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, template),
|
||
sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, template)
|
||
]);
|
||
} catch (error) {
|
||
console.error('[NOTIFICATIONS] Versand fehlgeschlagen:', error.message);
|
||
}
|
||
}
|
||
|
||
async function sendSlotNotification({ profileId, profileName, storeName, pickupDate, onlyNotify, booked, storeId }) {
|
||
const dateLabel = formatDateLabel(pickupDate);
|
||
const title = onlyNotify
|
||
? `Slot verfügbar bei ${storeName}`
|
||
: booked
|
||
? `Slot gebucht bei ${storeName}`
|
||
: `Slot gefunden bei ${storeName}`;
|
||
const baseMessage = onlyNotify
|
||
? `Es wurde ein freier Slot am ${dateLabel} entdeckt.`
|
||
: booked
|
||
? `Der Slot am ${dateLabel} wurde erfolgreich gebucht.`
|
||
: `Es wurde ein Slot am ${dateLabel} gefunden.`;
|
||
const storeLink = storeId ? `https://foodsharing.de/store/${storeId}` : null;
|
||
const fullMessage = storeLink ? `${baseMessage}\n${storeLink}` : baseMessage;
|
||
|
||
await notifyChannels(profileId, {
|
||
title,
|
||
message: fullMessage,
|
||
link: storeLink,
|
||
priority: booked ? 'high' : 'default',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
function formatStoreWatchStatus(status) {
|
||
if (status === 1) {
|
||
return 'Suchend';
|
||
}
|
||
if (status === 0) {
|
||
return 'Nicht suchend';
|
||
}
|
||
return 'Status unbekannt';
|
||
}
|
||
|
||
async function sendStoreWatchNotification({ profileId, profileName, storeName, storeId, regionName }) {
|
||
const storeLink = storeId ? `https://foodsharing.de/store/${storeId}` : null;
|
||
const title = `Team sucht Verstärkung: ${storeName}`;
|
||
const regionText = regionName ? ` (${regionName})` : '';
|
||
const messageBase = `Der Betrieb${regionText} sucht wieder aktiv neue Teammitglieder.`;
|
||
const message = storeLink ? `${messageBase}\n${storeLink}` : messageBase;
|
||
await notifyChannels(profileId, {
|
||
title,
|
||
message,
|
||
link: storeLink,
|
||
priority: 'high',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
async function sendStoreWatchSummaryNotification({ profileId, profileName, entries = [], triggeredBy = 'manual' }) {
|
||
if (!profileId || !Array.isArray(entries) || entries.length === 0) {
|
||
return;
|
||
}
|
||
const lines = entries
|
||
.map((entry) => {
|
||
const regionSuffix = entry.regionName ? ` (${entry.regionName})` : '';
|
||
const statusLabel = formatStoreWatchStatus(entry.status);
|
||
const timestamp = entry.checkedAt ? ` – Stand ${formatDateLabel(entry.checkedAt)}` : '';
|
||
const errorLabel = entry.error ? ` – Fehler: ${entry.error}` : '';
|
||
return `• ${entry.storeName || `Store ${entry.storeId}`}${regionSuffix}: ${statusLabel}${timestamp}${errorLabel}`;
|
||
})
|
||
.join('\n');
|
||
const prefix =
|
||
triggeredBy === 'manual'
|
||
? 'Manuell angestoßene Store-Watch-Aktualisierung abgeschlossen:'
|
||
: 'Store-Watch-Prüfung abgeschlossen:';
|
||
const title =
|
||
triggeredBy === 'manual' ? 'Store-Watch-Aktualisierung' : 'Store-Watch-Prüfung';
|
||
const message = `${prefix}\n${lines}`;
|
||
await notifyChannels(profileId, {
|
||
title,
|
||
message,
|
||
priority: 'default',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
async function sendDesiredWindowMissedNotification({ profileId, profileName, storeName, desiredWindowLabel }) {
|
||
if (!profileId) {
|
||
return;
|
||
}
|
||
const title = `Keine Slots gefunden: ${storeName}`;
|
||
const messageBase = desiredWindowLabel
|
||
? `Für den gewünschten Zeitraum ${desiredWindowLabel} konnte kein Slot gefunden werden.`
|
||
: 'Für den gewünschten Zeitraum konnte kein Slot gefunden werden.';
|
||
const message = `${messageBase} Der Eintrag wurde deaktiviert.`;
|
||
await notifyChannels(profileId, {
|
||
title,
|
||
message,
|
||
link: null,
|
||
priority: 'default',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
async function sendTestNotification(profileId, channel) {
|
||
const title = 'Pickup Benachrichtigung (Test)';
|
||
const message = 'Das ist eine Testnachricht. Bei Fragen wende dich bitte an den Admin.';
|
||
const adminSettings = adminConfig.readSettings();
|
||
const userSettings = readNotificationSettings(profileId);
|
||
const tasks = [];
|
||
|
||
if (!channel || channel === 'ntfy') {
|
||
tasks.push(
|
||
sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, {
|
||
title,
|
||
message,
|
||
priority: 'default'
|
||
})
|
||
);
|
||
}
|
||
if (!channel || channel === 'telegram') {
|
||
tasks.push(
|
||
sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, {
|
||
title,
|
||
message,
|
||
priority: 'default'
|
||
})
|
||
);
|
||
}
|
||
|
||
if (tasks.length === 0) {
|
||
throw new Error('Kein unterstützter Kanal oder Kanal deaktiviert.');
|
||
}
|
||
|
||
await Promise.all(tasks);
|
||
}
|
||
|
||
async function sendDormantPickupWarning({ profileId, profileName, storeName, storeId, reasonLines = [] }) {
|
||
if (!profileId || !Array.isArray(reasonLines) || reasonLines.length === 0) {
|
||
return;
|
||
}
|
||
const adminSettings = adminConfig.readSettings();
|
||
const userSettings = readNotificationSettings(profileId);
|
||
const storeLink = storeId ? `https://foodsharing.de/store/${storeId}` : null;
|
||
const title = `Prüfung fällig: ${storeName}`;
|
||
const messageBody = reasonLines.join('\n');
|
||
const message = storeLink ? `${messageBody}\n${storeLink}` : messageBody;
|
||
|
||
await sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, {
|
||
title,
|
||
message,
|
||
priority: 'high',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
async function sendJournalReminderNotification({
|
||
profileId,
|
||
profileName,
|
||
storeName,
|
||
pickupDate,
|
||
reminderDate,
|
||
note
|
||
}) {
|
||
if (!profileId) {
|
||
return;
|
||
}
|
||
const adminSettings = adminConfig.readSettings();
|
||
const userSettings = readNotificationSettings(profileId);
|
||
const title = `Erinnerung: Abholung bei ${storeName}`;
|
||
const reminderLabel = formatDateOnly(reminderDate);
|
||
const pickupLabel = formatDateOnly(pickupDate);
|
||
const noteLine = note ? `Notiz: ${note}` : null;
|
||
const messageLines = [
|
||
`Geplante Abholung: ${pickupLabel}`,
|
||
`Erinnerungstermin: ${reminderLabel}`,
|
||
noteLine
|
||
].filter(Boolean);
|
||
await sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, {
|
||
title,
|
||
message: messageLines.join('\n'),
|
||
priority: 'default',
|
||
profileName
|
||
});
|
||
}
|
||
|
||
async function sendAdminBookingErrorNotification({ profileId, profileEmail, storeName, storeId, pickupDate, error }) {
|
||
const dateLabel = formatDateLabel(pickupDate);
|
||
const storeLabel = storeName || storeId || 'Unbekannter Store';
|
||
const storeLink = storeId ? `https://foodsharing.de/store/${storeId}` : null;
|
||
const profileLabel = profileEmail || profileId || 'Unbekanntes Profil';
|
||
const messageLines = [
|
||
`Buchung fehlgeschlagen für ${storeLabel} am ${dateLabel}.`,
|
||
`Profil: ${profileLabel}.`,
|
||
`Fehler: ${error || 'Unbekannter Fehler'}.`
|
||
];
|
||
if (storeLink) {
|
||
messageLines.push(storeLink);
|
||
}
|
||
await sendAdminTelegramNotification({
|
||
title: 'Fehler beim Slot-Buchen',
|
||
message: messageLines.join('\n'),
|
||
priority: 'high'
|
||
});
|
||
}
|
||
|
||
async function sendAdminSessionErrorNotification({
|
||
profileId,
|
||
profileEmail,
|
||
profileName,
|
||
sessionId,
|
||
error,
|
||
label
|
||
}) {
|
||
const profileLabel = profileEmail || profileId || 'Unbekanntes Profil';
|
||
const messageLines = [
|
||
'Session-Login fehlgeschlagen.',
|
||
`Profil: ${profileLabel}.`,
|
||
profileName ? `Name: ${profileName}.` : null,
|
||
sessionId ? `Session: ${sessionId}.` : null,
|
||
label ? `Kontext: ${label}.` : null,
|
||
`Fehler: ${error || 'Unbekannter Fehler'}.`
|
||
].filter(Boolean);
|
||
await sendAdminTelegramNotification({
|
||
title: 'Fehler beim Session-Login',
|
||
message: messageLines.join('\n'),
|
||
priority: 'high'
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
sendSlotNotification,
|
||
sendStoreWatchNotification,
|
||
sendStoreWatchSummaryNotification,
|
||
sendTestNotification,
|
||
sendDesiredWindowMissedNotification,
|
||
sendDormantPickupWarning,
|
||
sendJournalReminderNotification,
|
||
sendAdminBookingErrorNotification,
|
||
sendAdminSessionErrorNotification
|
||
};
|