import Colle from '#models/colle' import Subscription from '#models/subscription' import env from '#start/env' import { DateTime } from 'luxon' import { UAParser } from 'ua-parser-js' import webpush, { PushSubscription } from 'web-push' export const EVENTS = { SYSTEM: 1 << 0, COLLE_ADDED: 1 << 1, COLLE_REMOVED: 1 << 2, COLLE_UPDATED: 1 << 3, GRADE_ADDED: 1 << 4, GRADE_UPDATED: 1 << 5, ROOM_UPDATED: 1 << 6, } type Event = (typeof EVENTS)[keyof typeof EVENTS] type NotificationId = keyof typeof NOTIFICATIONS export class NotificationService { constructor() { webpush.setVapidDetails( env.get('VAPID_DETAILS'), env.get('VAPID_PUBLIC_KEY'), env.get('VAPID_PRIVATE_KEY') ) } public setEvents(events: Event[]): number { return events.reduce((acc, event) => acc | event, 0) } public hasEvent(events: number, event: Event): boolean { return (events & event) !== 0 } private pushNotification(subscription: PushSubscription, payload: Record) { return webpush.sendNotification(subscription, JSON.stringify(payload)).catch((err) => { console.error('Error sending notification:', err) return false }) } public async sendNotification(notificationId: NotificationId, colle: Colle) { const payload = Object.assign(DEFAULT_NOTIFICATION, NOTIFICATIONS[notificationId](colle)) const subscriptions = await Subscription.query() .where('enabled', true) .where('userId', colle.studentId) // TODO: Check if working?? .whereRaw(`(events & ${EVENTS[notificationId]}) > 0`) for (const subscription of subscriptions) { await this.pushNotification(subscription.data, payload) // TODO: Count failed attempts and disable subscription if too many failures } } public async sendTestNotification(subscription: PushSubscription) { const payload = Object.assign(DEFAULT_NOTIFICATION, { title: 'Test Notification', body: 'Ceci est une notification de test.', }) return this.pushNotification(subscription, payload) } public getUserSignature(uaString: string) { const parser = new UAParser(uaString) const browser = parser.getBrowser() const os = parser.getOS() const device = parser.getDevice() const browserStr = browser.name ? `${browser.name} ${browser.version?.split('.')[0] || ''}`.trim() : '' const osStr = os.name ? `${os.name} ${os.version || ''}`.trim() : '' const deviceStr = device.model ? `${device.vendor || ''} ${device.model}`.trim() : device.type ? device.type : 'Desktop' return [deviceStr, osStr, browserStr].filter(Boolean).join(' | ') } } const NOTIFICATIONS = { COLLE_ADDED: (colle: Colle) => ({ title: 'Nouvelle colle', body: `Colle de ${colle.subject} ajoutée le ${formatDate(colle.date)}.`, data: { id: colle.id, }, actions: [OPEN_ACION, HOME_ACTION], }), COLLE_REMOVED: (colle: Colle) => ({ title: 'Colle supprimée', body: `Votre colle de ${colle.subject}, le ${formatDate(colle.date)} a été supprimée.`, actions: [HOME_ACTION], }), COLLE_UPDATED: (colle: Colle) => ({ title: 'Colle modifiée', body: `Votre colle de ${colle.subject} du ${formatDate(colle.date)} a été modifiée.`, data: { id: colle.id, }, actions: [OPEN_ACION, HOME_ACTION], }), GRADE_ADDED: (colle: Colle) => ({ title: 'Nouvelle note', body: `Colle de ${colle.subject} : ${colle.grade}/20`, data: { id: colle.id, }, actions: [OPEN_ACION, HOME_ACTION], }), GRADE_UPDATED: (colle: Colle) => ({ title: 'Note modifiée', body: `Colle de ${colle.subject} : ${colle.grade}/20`, data: { id: colle.id, }, actions: [OPEN_ACION, HOME_ACTION], }), ROOM_UPDATED: (colle: Colle) => ({ title: 'Salle modifiée', body: `Colle de ${colle.subject} en ${colle.room}.`, data: { id: colle.id, }, actions: [OPEN_ACION, HOME_ACTION], }), } const OPEN_ACION = { action: 'open', title: 'Ouvrir', } const HOME_ACTION = { action: 'home', title: 'Mes colles', } const DEFAULT_NOTIFICATION = { title: 'Notification', body: 'Vous avez une nouvelle notification.', requireInteraction: true, icon: env.get('PUBLIC_URL') + '/web-app-manifest-192x192.png', } const formatDate = (date: DateTime) => date.toJSDate().toLocaleDateString('fr-FR', { month: '2-digit', day: '2-digit', })