/// // Wer erfährt wovon — und was davon in die Warteschlange kommt. // // Eigene Datei, weil PocketBase jeden Hook in einer eigenen JS-Laufzeit // ausführt: Was neben onRecordAfterCreateSuccess steht, sieht der nächste // Handler nicht. Er holt sich seine Helfer per require(). const PREFS = 'notification_prefs' const QUEUE = 'notifications' /** * Was gilt, solange niemand etwas eingestellt hat. * * Die Linie ist bewusst gezogen: An heißt „betrifft mich", aus heißt „ist * viel". Wer zu einem Event eingetragen wird, will das wissen — das passiert * ihm. Jeder Kommentar an jedem Trail des Teams passiert dagegen ständig und * meistens anderen; wer das will, schaltet es ein. * * Diese Voreinstellungen gelten auch dann, wenn ein Konto nie in die * Einstellungen geschaut hat: Ein Datensatz in notification_prefs entsteht * erst beim ersten Speichern. */ const DEFAULTS = { enabled: true, event_created: true, // neues Event im Team event_participation: true, // du wurdest eingetragen, dein Status änderte sich event_changed: true, // Termin oder Ort eines Events verschoben trail_comment: false, // jeder Kommentar an jedem Trail trail_marker: false, // jede neue Meldung an der Strecke trail_status: true, // ein Trail wurde gesperrt oder wieder frei team_member: false, // jemand ist ins Team gekommen } /** Überschriften der Arten, wie sie in der Zusammenfassung stehen. */ const LABELS = { event_created: 'Neue Events', event_participation: 'Deine Teilnahmen', event_changed: 'Geänderte Events', trail_comment: 'Kommentare', trail_marker: 'Meldungen an der Strecke', trail_status: 'Trail-Status', team_member: 'Neu im Team', } /** * Einstellungen eines Kontos, ergänzt um alles, was nicht drinsteht. * * Ein fehlender Datensatz ist kein Sonderfall, sondern der Normalfall bei * jedem, der die Einstellungen nie geöffnet hat — er bekommt DEFAULTS. */ function prefsOf(app, userId) { let record = null try { record = app.findFirstRecordByFilter(PREFS, 'user = {:user}', { user: userId }) } catch (err) { record = null } if (!record) { return DEFAULTS } const out = { enabled: record.getBool('enabled') } for (const key in DEFAULTS) { if (key !== 'enabled') { out[key] = record.getBool(key) } } return out } /** Will dieses Konto diese Art von Meldung? */ function wants(app, userId, type) { const prefs = prefsOf(app, userId) return !!prefs.enabled && !!prefs[type] } /** * Die IDs einer Relation als gewöhnliches Array — wie im Verzeichnis-Modul. * `get()` gäbe je nach Feld einen String oder eine Go-Slice zurück. */ function idsOf(record, field) { const raw = record.getStringSlice(field) || [] const out = [] for (let i = 0; i < raw.length; i++) { if (raw[i]) { out.push(raw[i]) } } return out } /** Alle Konten eines Teams: Mitglieder und der Owner. */ function teamMembers(app, teamId) { let team try { team = app.findRecordById('teams', teamId) } catch (err) { return [] } const ids = idsOf(team, 'users') const owner = idsOf(team, 'owner')[0] if (owner && ids.indexOf(owner) === -1) { ids.push(owner) } return ids } /** * Einen Eintrag in die Warteschlange legen — für ein Konto. * * Wirft nie. Ein Auslöser hängt immer an einem Speichervorgang, den jemand * gerade ausgelöst hat: Ein neues Event darf nicht daran scheitern, dass ein * Hinweis nicht geschrieben werden konnte. */ function queueFor(app, userId, entry) { if (!userId || !entry || !entry.type) { return } // Der Auslöser selbst bekommt keine Meldung über sein eigenes Tun. Wer // ein Event anlegt, weiß, dass es eins gibt. if (entry.actor && entry.actor === userId) { return } try { if (!wants(app, userId, entry.type)) { return } const record = new Record(app.findCollectionByNameOrId(QUEUE)) record.set('user', userId) record.set('type', entry.type) record.set('title', entry.title || '') record.set('body', entry.body || '') record.set('link', entry.link || '') if (entry.team) { record.set('team', entry.team) } app.save(record) } catch (err) { console.log('[notifications] konnte nicht einreihen:', err) } } /** Denselben Eintrag für ein ganzes Team einreihen. */ function queueForTeam(app, teamId, entry) { const members = teamMembers(app, teamId) const withTeam = Object.assign({}, entry, { team: teamId }) for (let i = 0; i < members.length; i++) { queueFor(app, members[i], withTeam) } } /** * Das Konto hinter einem Fahrer — oder leer, wenn er keins hat. * Ohne Konto gibt es keine Adresse und damit nichts zu benachrichtigen. */ function userOfRider(app, riderId) { try { return app.findRecordById('riders', riderId).getString('user') } catch (err) { return '' } } /** Anzeigename eines Kontos für Sätze wie „X hat kommentiert". */ function nameOf(app, userId) { if (!userId) { return 'Jemand' } try { const u = app.findRecordById('users', userId) return u.getString('name') || u.getString('email') || 'Jemand' } catch (err) { return 'Jemand' } } /** * Einträge als versendet abhaken. Einzeln, damit ein hängender Datensatz * nicht die ganze Gruppe mitreißt und morgen alles doppelt rausgeht. */ function markSent(app, items) { const now = new Date().toISOString().replace('T', ' ') for (let i = 0; i < items.length; i++) { try { items[i].set('sent', now) app.save(items[i]) } catch (err) { console.log('[notifications] konnte nicht abhaken:', err) } } } module.exports = { PREFS: PREFS, QUEUE: QUEUE, DEFAULTS: DEFAULTS, LABELS: LABELS, prefsOf: prefsOf, wants: wants, idsOf: idsOf, teamMembers: teamMembers, queueFor: queueFor, queueForTeam: queueForTeam, userOfRider: userOfRider, nameOf: nameOf, markSent: markSent, }