api/app/services/notification_service.ts
2025-08-21 12:27:38 +02:00

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',
})