Files
Greenlens/server/lib/discord.js
2026-07-02 18:38:04 +02:00

103 lines
3.5 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 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 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(),
});
};
module.exports = {
isPurchaseEventType,
notifyPurchase,
notifyNewUser,
};