const axios = require('axios'); const adminConfig = require('./adminConfig'); const { readNotificationSettings } = require('./userSettingsStore'); 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' }); } catch (_error) { return 'unbekanntes Datum'; } } 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`; await axios.post( endpoint, { chat_id: userTelegram.chatId, text: payload.title ? `*${payload.title}*\n${payload.message}` : payload.message, 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, 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' }); } function formatStoreWatchStatus(status) { if (status === 1) { return 'Suchend'; } if (status === 0) { return 'Nicht suchend'; } return 'Status unbekannt'; } async function sendStoreWatchNotification({ profileId, 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' }); } async function sendStoreWatchSummaryNotification({ profileId, 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' }); } async function sendDesiredWindowMissedNotification({ profileId, 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' }); } 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, 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' }); } 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' }); } module.exports = { sendSlotNotification, sendStoreWatchNotification, sendStoreWatchSummaryNotification, sendTestNotification, sendDesiredWindowMissedNotification, sendDormantPickupWarning, sendAdminBookingErrorNotification };