209 lines
7.0 KiB
JavaScript
209 lines
7.0 KiB
JavaScript
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-Prüfung abgeschlossen:'
|
||
: 'Store-Watch-Prüfung abgeschlossen:';
|
||
const title =
|
||
triggeredBy === 'manual' ? 'Ad-hoc Store-Watch-Prüfung' : '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);
|
||
}
|
||
|
||
module.exports = {
|
||
sendSlotNotification,
|
||
sendStoreWatchNotification,
|
||
sendStoreWatchSummaryNotification,
|
||
sendTestNotification,
|
||
sendDesiredWindowMissedNotification
|
||
};
|