import fs from 'fs'; import User from '../models/community/user.js'; import PushNotificationDevice from '../models/community/push_notification_device.js'; import PushNotificationSetting from '../models/community/push_notification_setting.js'; const INVALID_TOKEN_CODES = new Set([ 'messaging/invalid-registration-token', 'messaging/registration-token-not-registered', ]); const EVENT_CONFIG = { chatMessage: { preference: 'chatEnabled', title: 'Neue Nachricht', body: 'Du hast eine neue Chat-Nachricht.', route: 'chat' }, friendloginchanged: { preference: 'friendLoginEnabled', title: 'Freund online', body: 'Ein Freund ist jetzt online.', route: 'social' }, familychanged: { preference: 'falukantEnabled', title: 'Falukant', body: 'Deine Familie hat Neuigkeiten.', route: 'falukant' }, falukantUpdateFamily: { preference: 'falukantEnabled', title: 'Falukant', body: 'Deine Familie wurde aktualisiert.', route: 'falukant' }, falukantUpdateStatus: { preference: 'falukantEnabled', title: 'Falukant', body: 'Es gibt einen neuen Falukant-Status.', route: 'falukant' }, vocabReminder: { preference: 'vocabReminderEnabled', title: 'Vokabeltraining', body: 'Deine Wiederholung wartet auf dich.', route: 'vocab' }, }; let messagingPromise; async function getMessaging() { if (messagingPromise !== undefined) return messagingPromise; messagingPromise = (async () => { const inlineCredentials = process.env.FIREBASE_SERVICE_ACCOUNT_JSON; const credentialPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH; if (!inlineCredentials && !credentialPath) return null; try { const { cert, getApps, initializeApp } = await import('firebase-admin/app'); const { getMessaging: getFirebaseMessaging } = await import('firebase-admin/messaging'); const serviceAccount = JSON.parse(inlineCredentials || fs.readFileSync(credentialPath, 'utf8')); const app = getApps()[0] || initializeApp({ credential: cert(serviceAccount) }); return getFirebaseMessaging(app); } catch (error) { console.error('[push] Firebase Admin konnte nicht initialisiert werden:', error.message); return null; } })(); return messagingPromise; } async function getUserByHashedId(hashedUserId) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) throw new Error('usernotfound'); return user; } async function getSettings(userId) { const [settings] = await PushNotificationSetting.findOrCreate({ where: { userId } }); return settings; } export async function registerDevice(hashedUserId, { token, enabled = true }) { const user = await getUserByHashedId(hashedUserId); const device = await PushNotificationDevice.findOne({ where: { token } }); if (device) { await device.update({ userId: user.id, platform: 'android', enabled }); } else { await PushNotificationDevice.create({ userId: user.id, token, platform: 'android', enabled }); } return { registered: true }; } export async function disableDevice(hashedUserId, token) { const user = await getUserByHashedId(hashedUserId); await PushNotificationDevice.update({ enabled: false }, { where: { userId: user.id, token } }); return { disabled: true }; } export async function loadSettings(hashedUserId) { const user = await getUserByHashedId(hashedUserId); return (await getSettings(user.id)).toJSON(); } export async function updateSettings(hashedUserId, values) { const user = await getUserByHashedId(hashedUserId); const settings = await getSettings(user.id); await settings.update(values); return settings.toJSON(); } export async function dispatchRealtimeEvent(hashedUserId, event, data = {}) { const config = EVENT_CONFIG[event]; if (!config) return; const user = await getUserByHashedId(hashedUserId); const settings = await getSettings(user.id); if (!settings.enabled || !settings[config.preference]) return; const messaging = await getMessaging(); if (!messaging) return; const devices = await PushNotificationDevice.findAll({ where: { userId: user.id, enabled: true, platform: 'android' } }); await Promise.all(devices.map(async (device) => { try { await messaging.send({ token: device.token, notification: { title: config.title, body: config.body }, data: { route: config.route, event, ...Object.fromEntries(Object.entries(data || {}).map(([key, value]) => [key, String(value)])) }, android: { priority: 'high', notification: { channelId: 'yourpart_updates' } }, }); } catch (error) { if (INVALID_TOKEN_CODES.has(error.code)) { await device.destroy(); } else { console.error('[push] Zustellung fehlgeschlagen:', error.message); } } })); }