182 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			182 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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'
 | |
| 
 | |
| const MAX_FAILED_ATTEMPTS = 5
 | |
| 
 | |
| 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<string, any>) {
 | |
|     return webpush.sendNotification(subscription, JSON.stringify(payload))
 | |
|   }
 | |
| 
 | |
|   public async sendNotification(notificationId: NotificationId, colle: Colle, args?: any) {
 | |
|     await colle.load('subject')
 | |
|     if (notificationId === 'ROOM_UPDATED') {
 | |
|       await colle.load('room')
 | |
|     }
 | |
|     const payload = Object.assign(DEFAULT_NOTIFICATION, NOTIFICATIONS[notificationId](colle, args))
 | |
| 
 | |
|     const subscriptions = await Subscription.query()
 | |
|       .where('enabled', true)
 | |
|       .where('userId', colle.studentId)
 | |
|       .whereRaw(`(events & ${EVENTS[notificationId]}) > 0`)
 | |
| 
 | |
|     for (const subscription of subscriptions) {
 | |
|       try {
 | |
|         await this.pushNotification(subscription.data, payload)
 | |
|         // Reset failed attempts on successful notification
 | |
|         if (subscription.failedAttempts) {
 | |
|           subscription.failedAttempts = 0
 | |
|           await subscription.save()
 | |
|         }
 | |
|       } catch (err) {
 | |
|         console.error(
 | |
|           `Error sending notification for ${notificationId} to user ${colle.studentId}:`,
 | |
|           err
 | |
|         )
 | |
|         // Increment failed attempts and disable subscription if too many failures
 | |
|         subscription.failedAttempts = (subscription.failedAttempts || 0) + 1
 | |
|         if (subscription.failedAttempts >= MAX_FAILED_ATTEMPTS) {
 | |
|           subscription.enabled = false
 | |
|         }
 | |
|         await subscription.save()
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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.name} 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.name}, 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.name} 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.name} : ${colle.grade}/20`,
 | |
|     data: {
 | |
|       id: colle.id,
 | |
|     },
 | |
|     actions: [OPEN_ACION, HOME_ACTION],
 | |
|   }),
 | |
|   GRADE_UPDATED: (colle: Colle, oldGrade: number) => ({
 | |
|     title: 'Note modifiée',
 | |
|     body: `Colle de ${colle.subject.name} : ${oldGrade}/20 --> ${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.name} en ${colle.room.name}.`,
 | |
|     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',
 | |
|   })
 | 
