api/app/services/notification_service.ts
2025-08-21 10:49:56 +02:00

167 lines
4.6 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'
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)).catch((err) => {
console.error('Error sending notification:', err)
return false
})
}
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)
// 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.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',
})