Files
Pickup-Config/services/notificationService.js

228 lines
7.7 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 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 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'
});
}
module.exports = {
sendSlotNotification,
sendStoreWatchNotification,
sendStoreWatchSummaryNotification,
sendTestNotification,
sendDesiredWindowMissedNotification,
sendDormantPickupWarning
};