unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
1import { Op } from 'sequelize'
2import {
3 getNotificationBody,
4 getNotificationTitle,
5 NotificationBody,
6 NotificationContext
7} from './pushNotifications.js'
8import { UnifiedPushData } from '../models/unifiedPushData.js'
9import { getMutedPostsMultiple } from './cacheGetters/getMutedPosts.js'
10import WebPush from 'web-push'
11import { logger } from './logger.js'
12import { completeEnvironment } from './backendOptions.js'
13
14WebPush.setVapidDetails(
15 completeEnvironment.webpushEmail,
16 completeEnvironment.webpushPublicKey,
17 completeEnvironment.webpushPrivateKey
18)
19
20export async function sendWebPushNotifications(notifications: NotificationBody[], context?: NotificationContext) {
21 const userIds = notifications.map((elem) => elem.notifiedUserId)
22 const pushDataRows = await UnifiedPushData.findAll({
23 where: {
24 userId: {
25 [Op.in]: userIds
26 }
27 }
28 })
29 const mutedPosts = await getMutedPostsMultiple(userIds, true)
30
31 for (const notification of notifications) {
32 const userId = notification.notifiedUserId
33 const mutes = mutedPosts.get(userId)
34 const isMuted = mutes && notification.postId ? mutes.includes(notification.postId) : false
35 if (!isMuted) {
36 const userDevices = pushDataRows.filter((p) => p.userId === userId)
37 for (const device of userDevices) {
38 await sendWebPushNotification(notification, device, context)
39 }
40 }
41 }
42}
43
44async function sendWebPushNotification(
45 notification: NotificationBody,
46 device: UnifiedPushData,
47 context?: NotificationContext
48) {
49 try {
50 const payload = await getNotificationPayload(notification, context)
51 await WebPush.sendNotification(
52 {
53 endpoint: device.endpoint,
54 keys: {
55 auth: device.deviceAuth,
56 p256dh: device.devicePublicKey
57 }
58 },
59 JSON.stringify(payload)
60 )
61 } catch (error) {
62 logger.error({ message: 'Error sending web push notification: ', error: error })
63 // TODO: if sending webpush fails, we can remove this record from the db
64 // if the error is some weird error and we should not have removed the record
65 // that is not a problem, because next time the app opens, the device will be registered again
66 await UnifiedPushData.destroy({
67 where: {
68 endpoint: device.endpoint
69 }
70 })
71 }
72}
73
74// TODO: need a simpler way to generate a unique number id from a string
75// maybe create the notification record in the db first and then send the notification to the devices
76function getNotificationId(notification: NotificationBody) {
77 const key = `${notification.notifiedUserId}-${notification.userId}-${notification.notificationType}-${notification.postId}`
78 return crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)).then((hash) => {
79 return new Uint32Array(hash).at(0)!
80 })
81}
82
83function getNotificationUrl(notification: NotificationBody, context?: NotificationContext) {
84 if (notification.notificationType === 'FOLLOW') {
85 return `wafrn://user/${context?.userUrl}`
86 } else {
87 return `wafrn://post/${notification.postId}`
88 }
89}
90
91async function getNotificationPayload(notification: NotificationBody, context?: NotificationContext) {
92 return {
93 id: await getNotificationId(notification),
94 url: getNotificationUrl(notification, context),
95 title: getNotificationTitle(notification, context),
96 body: getNotificationBody(notification, context),
97 type: notification.notificationType
98 }
99}