unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 151 lines 5.3 kB view raw
1import { Expo, type ExpoPushErrorTicket } from 'expo-server-sdk' 2import { logger } from './logger.js' 3import { Notification, PushNotificationToken } from '../models/index.js' 4import { Queue } from 'bullmq' 5import { getAllLocalUserIds } from './cacheGetters/getAllLocalUserIds.js' 6import dompurify from 'isomorphic-dompurify' 7import { completeEnvironment } from './backendOptions.js' 8 9type PushNotificationPayload = { 10 notifications: NotificationBody[] 11 context?: NotificationContext 12} 13 14const sendPushNotificationQueue = new Queue<PushNotificationPayload>('sendPushNotification', { 15 connection: completeEnvironment.bullmqConnection, 16 defaultJobOptions: { 17 removeOnComplete: true, 18 attempts: 3, 19 backoff: { 20 type: 'exponential', 21 delay: 1000 22 } 23 } 24}) 25 26export type NotificationBody = { 27 notifiedUserId: string 28 userId: string 29 notificationType: 'FOLLOW' | 'LIKE' | 'REWOOT' | 'MENTION' | 'QUOTE' | 'EMOJIREACT' | 'USERBITE' | 'POSTBITE' 30 postId?: string 31 emojiReactionId?: string 32 createdAt?: Date 33 updatedAt?: Date 34} 35 36export type NotificationContext = { 37 postContent?: string 38 userUrl?: string 39 emoji?: string 40 ignoreDuplicates?: boolean 41} 42 43export async function deleteToken(token: string) { 44 return PushNotificationToken.destroy({ 45 where: { 46 token 47 } 48 }) 49} 50 51const expoClient = new Expo() 52 53export async function bulkCreateNotifications(notifications: NotificationBody[], context?: NotificationContext) { 54 const localUserIds = await getAllLocalUserIds() 55 const localUserNotifications = notifications.filter((elem) => localUserIds.includes(elem.notifiedUserId)) 56 if (localUserNotifications.length > 0) { 57 if (context && context.postContent) { 58 context.postContent = dompurify.sanitize(context.postContent, { ALLOWED_TAGS: [] }) 59 } 60 const notificationDate = notifications[0].createdAt ? notifications[0].createdAt : new Date() 61 const timeDiff = Math.abs(new Date().getTime() - notificationDate.getTime()) 62 const sendNotifications = 63 timeDiff < 3600 * 1000 ? sendPushNotificationQueue.add('sendPushNotification', { notifications, context }) : null 64 await Promise.all([ 65 Notification.bulkCreate(localUserNotifications, { ignoreDuplicates: context?.ignoreDuplicates }), 66 sendNotifications 67 ]) 68 } 69} 70 71export async function createNotification(notification: NotificationBody, context?: NotificationContext) { 72 const localUserIds = await getAllLocalUserIds() 73 if (localUserIds.includes(notification.notifiedUserId)) { 74 if (notification.postId && notification.notificationType != 'EMOJIREACT' 75 && notification.notificationType != 'USERBITE' && notification.notificationType != 'POSTBITE') { 76 // lets avoid double existing notifications. Ok may break things with emojireacts and bites 77 const existingNotifications = await Notification.count({ 78 where: { 79 userId: notification.userId, 80 notifiedUserId: notification.notifiedUserId, 81 postId: notification.postId, 82 notificationType: notification.notificationType 83 } 84 }) 85 if (existingNotifications) { 86 return 87 } 88 } 89 if (context && context.postContent) { 90 context.postContent = dompurify.sanitize(context.postContent, { ALLOWED_TAGS: [] }) 91 } 92 const notificationDate = notification.createdAt ? notification.createdAt : new Date() 93 const timeDiff = Math.abs(new Date().getTime() - notificationDate.getTime()) 94 const sendNotification = 95 timeDiff < 3600 * 1000 96 ? sendPushNotificationQueue.add('sendPushNotification', { 97 notifications: [notification], 98 context 99 }) 100 : null 101 await Promise.all([Notification.create(notification), sendNotification]) 102 } 103} 104 105// Error codes reference: https://docs.expo.io/push-notifications/sending-notifications/#individual-errors 106export async function handleDeliveryError(response: ExpoPushErrorTicket) { 107 logger.error(response) 108 const error = response.details?.error 109 110 // do not send notifications again to this token until it is registered again 111 if (error === 'DeviceNotRegistered') { 112 const token = response.details?.expoPushToken 113 if (token) { 114 await deleteToken(token) 115 } 116 } 117} 118 119const verbMap = { 120 LIKE: 'liked', 121 REWOOT: 'rewooted', 122 MENTION: 'replied to', 123 QUOTE: 'quoted', 124 EMOJIREACT: 'reacted to', 125 USERBITE: 'bit', 126 POSTBITE: 'bit' 127} 128 129export function getNotificationTitle(notification: NotificationBody, context?: NotificationContext) { 130 if (notification.notificationType === 'FOLLOW') { 131 return 'New user followed you' 132 } 133 134 if (notification.notificationType === 'USERBITE') { 135 return `${context?.userUrl || 'someone'} bit you` 136 } 137 138 if (notification.notificationType === 'EMOJIREACT' && context?.emoji) { 139 return `${context?.userUrl || 'someone'} reacted with ${context.emoji} to your post` 140 } 141 142 return `${context?.userUrl || 'someone'} ${verbMap[notification.notificationType]} your post` 143} 144 145export function getNotificationBody(notification: NotificationBody, context?: NotificationContext) { 146 if (notification.notificationType === 'FOLLOW' || notification.notificationType === 'USERBITE') { 147 return context?.userUrl ? `@${context?.userUrl.replace(/^@/, '')}` : '' 148 } 149 150 return `${context?.postContent}` 151}