const SALES_WEBHOOK_URL = (process.env.DISCORD_WEBHOOK_SALES_URL || '').trim(); const DOWNLOADS_WEBHOOK_URL = (process.env.DISCORD_WEBHOOK_DOWNLOADS_URL || '').trim(); const PURCHASE_EVENT_TYPES = new Set([ 'INITIAL_PURCHASE', 'RENEWAL', 'NON_RENEWING_PURCHASE', 'PRODUCT_CHANGE', ]); const PLAN_NAMES_BY_PRODUCT = { monthly_pro: 'Pro (monatlich)', yearly_pro: 'Pro (jährlich)', topup_small: 'Top-up Small (30 Credits)', topup_medium: 'Top-up Medium (100 Credits)', topup_large: 'Top-up Large (250 Credits)', }; const EMBED_COLOR_SALE = 0x2ecc71; const EMBED_COLOR_NEW_USER = 0x3498db; const EMBED_COLOR_DOWNLOAD = 0x9b59b6; const isPurchaseEventType = (eventType) => PURCHASE_EVENT_TYPES.has(String(eventType || '').toUpperCase()); // Header/payload values end up in Discord embeds; strip markdown-relevant // characters and cap length so user-controlled input cannot break or spoof // the embed (Discord rejects fields over 1024 chars). const sanitizeEmbedText = (value, maxLength = 64) => String(value ?? '').replace(/[\r\n`]/g, ' ').trim().slice(0, maxLength); const formatStore = (store) => { const normalized = String(store || '').toUpperCase(); if (normalized === 'APP_STORE' || normalized === 'MAC_APP_STORE') return 'iOS'; if (normalized === 'PLAY_STORE') return 'Android'; return normalized || 'Unbekannt'; }; const formatPlatform = (platform) => { const normalized = String(platform || '').toLowerCase(); if (normalized === 'ios') return 'iOS'; if (normalized === 'android') return 'Android'; return 'Unbekannt'; }; const formatPrice = (price, currency) => { if (typeof price !== 'number' || !Number.isFinite(price)) return '–'; return `${price.toFixed(2)} ${String(currency || 'USD').toUpperCase()}`; }; const sendDiscordEmbed = (webhookUrl, embed) => { if (!webhookUrl) return; fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ embeds: [embed] }), signal: AbortSignal.timeout(5000), }) // Drain the body so undici can release the connection back to the pool. .then((response) => response.arrayBuffer()) .catch(() => {}); }; const notifyPurchase = ({ productId, price, currency, store, isTrial, isRenewal, isSandbox } = {}) => { const safeProductId = sanitizeEmbedText(productId); const planName = Object.hasOwn(PLAN_NAMES_BY_PRODUCT, safeProductId) ? PLAN_NAMES_BY_PRODUCT[safeProductId] : safeProductId || 'Unbekannt'; const title = isTrial ? '🛍️ Trial gestartet' : isRenewal ? '🔁 Abo verlängert' : '🛍️ Neuer Kauf'; sendDiscordEmbed(SALES_WEBHOOK_URL, { title: isSandbox ? `${title} (Sandbox)` : title, color: EMBED_COLOR_SALE, fields: [ { name: 'Produkt', value: `${planName} (\`${safeProductId || '–'}\`)`, inline: true }, { name: 'Preis', value: isTrial ? 'Trial' : formatPrice(price, currency), inline: true }, { name: 'Gerät', value: formatStore(store), inline: true }, ], timestamp: new Date().toISOString(), }); }; const notifyNewUser = ({ platform, appVersion, provider } = {}) => { sendDiscordEmbed(DOWNLOADS_WEBHOOK_URL, { title: '📲 Neuer User', color: EMBED_COLOR_NEW_USER, fields: [ { name: 'Gerät', value: formatPlatform(platform), inline: true }, { name: 'App-Version', value: sanitizeEmbedText(appVersion) || 'Unbekannt', inline: true }, { name: 'Anmeldung', value: String(provider || 'Unbekannt'), inline: true }, ], timestamp: new Date().toISOString(), }); }; const notifyDownload = ({ platform, appVersion } = {}) => { sendDiscordEmbed(DOWNLOADS_WEBHOOK_URL, { title: '⬇️ Neuer Download', color: EMBED_COLOR_DOWNLOAD, fields: [ { name: 'Gerät', value: formatPlatform(platform), inline: true }, { name: 'App-Version', value: sanitizeEmbedText(appVersion) || 'Unbekannt', inline: true }, ], timestamp: new Date().toISOString(), }); }; module.exports = { isPurchaseEventType, notifyPurchase, notifyNewUser, notifyDownload, };