unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at testPDSNotExplode 144 lines 5.0 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' 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 // lets avoid double existing notifications. Ok may break things with emojireacts 76 const existingNotifications = await Notification.count({ 77 where: { 78 userId: notification.userId, 79 notifiedUserId: notification.notifiedUserId, 80 postId: notification.postId, 81 notificationType: notification.notificationType 82 } 83 }) 84 if (existingNotifications) { 85 return 86 } 87 } 88 if (context && context.postContent) { 89 context.postContent = dompurify.sanitize(context.postContent, { ALLOWED_TAGS: [] }) 90 } 91 const notificationDate = notification.createdAt ? notification.createdAt : new Date() 92 const timeDiff = Math.abs(new Date().getTime() - notificationDate.getTime()) 93 const sendNotification = 94 timeDiff < 3600 * 1000 95 ? sendPushNotificationQueue.add('sendPushNotification', { 96 notifications: [notification], 97 context 98 }) 99 : null 100 await Promise.all([Notification.create(notification), sendNotification]) 101 } 102} 103 104// Error codes reference: https://docs.expo.io/push-notifications/sending-notifications/#individual-errors 105export async function handleDeliveryError(response: ExpoPushErrorTicket) { 106 logger.error(response) 107 const error = response.details?.error 108 109 // do not send notifications again to this token until it is registered again 110 if (error === 'DeviceNotRegistered') { 111 const token = response.details?.expoPushToken 112 if (token) { 113 await deleteToken(token) 114 } 115 } 116} 117 118const verbMap = { 119 LIKE: 'liked', 120 REWOOT: 'rewooted', 121 MENTION: 'replied to', 122 QUOTE: 'quoted', 123 EMOJIREACT: 'reacted to' 124} 125 126export function getNotificationTitle(notification: NotificationBody, context?: NotificationContext) { 127 if (notification.notificationType === 'FOLLOW') { 128 return 'New user followed you' 129 } 130 131 if (notification.notificationType === 'EMOJIREACT' && context?.emoji) { 132 return `${context?.userUrl || 'someone'} reacted with ${context.emoji} to your post` 133 } 134 135 return `${context?.userUrl || 'someone'} ${verbMap[notification.notificationType]} your post` 136} 137 138export function getNotificationBody(notification: NotificationBody, context?: NotificationContext) { 139 if (notification.notificationType === 'FOLLOW') { 140 return context?.userUrl ? `@${context?.userUrl.replace(/^@/, '')}` : '' 141 } 142 143 return `${context?.postContent}` 144}