Put all Web Push code into lib/webPush.js (#936)
* Rename file to webPush.js * Move webPush code into lib/webPush
This commit is contained in:
parent
2f9a3cc12c
commit
b03295ce59
|
@ -15,9 +15,8 @@ import { msatsToSats } from '../../lib/format'
|
|||
import { parse } from 'tldts'
|
||||
import uu from 'url-unshort'
|
||||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
import { sendUserNotification } from '../webPush'
|
||||
import { sendUserNotification, notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers } from '../../lib/webPush'
|
||||
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
|
||||
import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers } from '../../lib/push-notifications'
|
||||
import { datePivot, whenRange } from '../../lib/time'
|
||||
import { imageFeesInfo, uploadIdsFromText } from './image'
|
||||
import assertGofacYourself from './ofac'
|
||||
|
|
|
@ -3,7 +3,7 @@ import { decodeCursor, LIMIT, nextNoteCursorEncoded } from '../../lib/cursor'
|
|||
import { getItem, filterClause, whereClause, muteClause } from './item'
|
||||
import { getInvoice } from './wallet'
|
||||
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
|
||||
import { replyToSubscription } from '../webPush'
|
||||
import { replyToSubscription } from '../../lib/webPush'
|
||||
import { getSub } from './sub'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ssValidate, territorySchema } from '../../lib/validate'
|
|||
import { nextBilling, proratedBillingCost } from '../../lib/territory'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { subViewGroup } from './growth'
|
||||
import { notifyTerritoryTransfer } from '../../lib/push-notifications'
|
||||
import { notifyTerritoryTransfer } from '../../lib/webPush'
|
||||
|
||||
export function paySubQueries (sub, models) {
|
||||
if (sub.billingType === 'ONCE') {
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import webPush from 'web-push'
|
||||
import models from '../models'
|
||||
import { COMMENT_DEPTH_LIMIT } from '../../lib/constants'
|
||||
import removeMd from 'remove-markdown'
|
||||
|
||||
const webPushEnabled = process.env.NODE_ENV === 'production' ||
|
||||
(process.env.VAPID_MAILTO && process.env.NEXT_PUBLIC_VAPID_PUBKEY && process.env.VAPID_PRIVKEY)
|
||||
|
||||
if (webPushEnabled) {
|
||||
webPush.setVapidDetails(
|
||||
process.env.VAPID_MAILTO,
|
||||
process.env.NEXT_PUBLIC_VAPID_PUBKEY,
|
||||
process.env.VAPID_PRIVKEY
|
||||
)
|
||||
} else {
|
||||
console.warn('VAPID_* env vars not set, skipping webPush setup')
|
||||
}
|
||||
|
||||
const createPayload = (notification) => {
|
||||
// https://web.dev/push-notifications-display-a-notification/#visual-options
|
||||
let { title, body, ...options } = notification
|
||||
if (body) body = removeMd(body)
|
||||
return JSON.stringify({
|
||||
title,
|
||||
options: {
|
||||
body,
|
||||
timestamp: Date.now(),
|
||||
icon: '/icons/icon_x96.png',
|
||||
...options
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createUserFilter = (tag) => {
|
||||
// filter users by notification settings
|
||||
const tagMap = {
|
||||
REPLY: 'noteAllDescendants',
|
||||
MENTION: 'noteMentions',
|
||||
TIP: 'noteItemSats',
|
||||
FORWARDEDTIP: 'noteForwardedSats',
|
||||
REFERRAL: 'noteInvites',
|
||||
INVITE: 'noteInvites',
|
||||
EARN: 'noteEarning',
|
||||
DEPOSIT: 'noteDeposits',
|
||||
STREAK: 'noteCowboyHat'
|
||||
}
|
||||
const key = tagMap[tag.split('-')[0]]
|
||||
return key ? { user: { [key]: true } } : undefined
|
||||
}
|
||||
|
||||
const createItemUrl = async ({ id }) => {
|
||||
const [rootItem] = await models.$queryRawUnsafe(
|
||||
'SELECT subpath(path, -LEAST(nlevel(path), $1::INTEGER), 1)::text AS id FROM "Item" WHERE id = $2::INTEGER',
|
||||
COMMENT_DEPTH_LIMIT + 1, Number(id)
|
||||
)
|
||||
return `/items/${rootItem.id}` + (rootItem.id !== id ? `?commentId=${id}` : '')
|
||||
}
|
||||
|
||||
const sendNotification = (subscription, payload) => {
|
||||
if (!webPushEnabled) {
|
||||
console.warn('webPush not configured. skipping notification')
|
||||
return
|
||||
}
|
||||
const { id, endpoint, p256dh, auth } = subscription
|
||||
return webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, payload)
|
||||
.catch(async (err) => {
|
||||
if (err.statusCode === 400) {
|
||||
console.log('[webPush] invalid request: ', err)
|
||||
} else if ([401, 403].includes(err.statusCode)) {
|
||||
console.log('[webPush] auth error: ', err)
|
||||
} else if (err.statusCode === 404 || err.statusCode === 410) {
|
||||
console.log('[webPush] subscription has expired or is no longer valid: ', err)
|
||||
const deletedSubscripton = await models.pushSubscription.delete({ where: { id } })
|
||||
console.log(`[webPush] deleted subscription ${id} of user ${deletedSubscripton.userId} due to push error`)
|
||||
} else if (err.statusCode === 413) {
|
||||
console.log('[webPush] payload too large: ', err)
|
||||
} else if (err.statusCode === 429) {
|
||||
console.log('[webPush] too many requests: ', err)
|
||||
} else {
|
||||
console.log('[webPush] error: ', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendUserNotification (userId, notification) {
|
||||
try {
|
||||
notification.data ??= {}
|
||||
if (notification.item) {
|
||||
notification.data.url ??= await createItemUrl(notification.item)
|
||||
notification.data.itemId ??= notification.item.id
|
||||
delete notification.item
|
||||
}
|
||||
const userFilter = createUserFilter(notification.tag)
|
||||
const payload = createPayload(notification)
|
||||
const subscriptions = await models.pushSubscription.findMany({
|
||||
where: { userId, ...userFilter }
|
||||
})
|
||||
await Promise.allSettled(
|
||||
subscriptions.map(subscription => sendNotification(subscription, payload))
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('[webPush] error sending user notification: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function replyToSubscription (subscriptionId, notification) {
|
||||
try {
|
||||
const payload = createPayload(notification)
|
||||
const subscription = await models.pushSubscription.findUnique({ where: { id: subscriptionId } })
|
||||
await sendNotification(subscription, payload)
|
||||
} catch (err) {
|
||||
console.log('[webPush] error sending subscription reply: ', err)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,118 @@
|
|||
import { sendUserNotification } from '../api/webPush'
|
||||
import { ANON_USER_ID } from './constants'
|
||||
import webPush from 'web-push'
|
||||
import removeMd from 'remove-markdown'
|
||||
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT } from './constants'
|
||||
import { msatsToSats, numWithUnits } from './format'
|
||||
import models from '../api/models'
|
||||
|
||||
const webPushEnabled = process.env.NODE_ENV === 'production' ||
|
||||
(process.env.VAPID_MAILTO && process.env.NEXT_PUBLIC_VAPID_PUBKEY && process.env.VAPID_PRIVKEY)
|
||||
|
||||
if (webPushEnabled) {
|
||||
webPush.setVapidDetails(
|
||||
process.env.VAPID_MAILTO,
|
||||
process.env.NEXT_PUBLIC_VAPID_PUBKEY,
|
||||
process.env.VAPID_PRIVKEY
|
||||
)
|
||||
} else {
|
||||
console.warn('VAPID_* env vars not set, skipping webPush setup')
|
||||
}
|
||||
|
||||
const createPayload = (notification) => {
|
||||
// https://web.dev/push-notifications-display-a-notification/#visual-options
|
||||
let { title, body, ...options } = notification
|
||||
if (body) body = removeMd(body)
|
||||
return JSON.stringify({
|
||||
title,
|
||||
options: {
|
||||
body,
|
||||
timestamp: Date.now(),
|
||||
icon: '/icons/icon_x96.png',
|
||||
...options
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createUserFilter = (tag) => {
|
||||
// filter users by notification settings
|
||||
const tagMap = {
|
||||
REPLY: 'noteAllDescendants',
|
||||
MENTION: 'noteMentions',
|
||||
TIP: 'noteItemSats',
|
||||
FORWARDEDTIP: 'noteForwardedSats',
|
||||
REFERRAL: 'noteInvites',
|
||||
INVITE: 'noteInvites',
|
||||
EARN: 'noteEarning',
|
||||
DEPOSIT: 'noteDeposits',
|
||||
STREAK: 'noteCowboyHat'
|
||||
}
|
||||
const key = tagMap[tag.split('-')[0]]
|
||||
return key ? { user: { [key]: true } } : undefined
|
||||
}
|
||||
|
||||
const createItemUrl = async ({ id }) => {
|
||||
const [rootItem] = await models.$queryRawUnsafe(
|
||||
'SELECT subpath(path, -LEAST(nlevel(path), $1::INTEGER), 1)::text AS id FROM "Item" WHERE id = $2::INTEGER',
|
||||
COMMENT_DEPTH_LIMIT + 1, Number(id)
|
||||
)
|
||||
return `/items/${rootItem.id}` + (rootItem.id !== id ? `?commentId=${id}` : '')
|
||||
}
|
||||
|
||||
const sendNotification = (subscription, payload) => {
|
||||
if (!webPushEnabled) {
|
||||
console.warn('webPush not configured. skipping notification')
|
||||
return
|
||||
}
|
||||
const { id, endpoint, p256dh, auth } = subscription
|
||||
return webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, payload)
|
||||
.catch(async (err) => {
|
||||
if (err.statusCode === 400) {
|
||||
console.log('[webPush] invalid request: ', err)
|
||||
} else if ([401, 403].includes(err.statusCode)) {
|
||||
console.log('[webPush] auth error: ', err)
|
||||
} else if (err.statusCode === 404 || err.statusCode === 410) {
|
||||
console.log('[webPush] subscription has expired or is no longer valid: ', err)
|
||||
const deletedSubscripton = await models.pushSubscription.delete({ where: { id } })
|
||||
console.log(`[webPush] deleted subscription ${id} of user ${deletedSubscripton.userId} due to push error`)
|
||||
} else if (err.statusCode === 413) {
|
||||
console.log('[webPush] payload too large: ', err)
|
||||
} else if (err.statusCode === 429) {
|
||||
console.log('[webPush] too many requests: ', err)
|
||||
} else {
|
||||
console.log('[webPush] error: ', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendUserNotification (userId, notification) {
|
||||
try {
|
||||
notification.data ??= {}
|
||||
if (notification.item) {
|
||||
notification.data.url ??= await createItemUrl(notification.item)
|
||||
notification.data.itemId ??= notification.item.id
|
||||
delete notification.item
|
||||
}
|
||||
const userFilter = createUserFilter(notification.tag)
|
||||
const payload = createPayload(notification)
|
||||
const subscriptions = await models.pushSubscription.findMany({
|
||||
where: { userId, ...userFilter }
|
||||
})
|
||||
await Promise.allSettled(
|
||||
subscriptions.map(subscription => sendNotification(subscription, payload))
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('[webPush] error sending user notification: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function replyToSubscription (subscriptionId, notification) {
|
||||
try {
|
||||
const payload = createPayload(notification)
|
||||
const subscription = await models.pushSubscription.findUnique({ where: { id: subscriptionId } })
|
||||
await sendNotification(subscription, payload)
|
||||
} catch (err) {
|
||||
console.log('[webPush] error sending subscription reply: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyUserSubscribers = async ({ models, item }) => {
|
||||
try {
|
|
@ -10,7 +10,7 @@ import { PrismaAdapter } from '@auth/prisma-adapter'
|
|||
import { getToken } from 'next-auth/jwt'
|
||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
||||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { sendUserNotification } from '../../../api/webPush'
|
||||
import { sendUserNotification } from '../../../lib/webPush'
|
||||
|
||||
/**
|
||||
* Stores userIds in user table
|
||||
|
|
|
@ -9,7 +9,7 @@ import getSSRApolloClient from '../../api/ssrApollo'
|
|||
import Link from 'next/link'
|
||||
import { CenterLayout } from '../../components/layout'
|
||||
import { getAuthOptions } from '../api/auth/[...nextauth]'
|
||||
import { sendUserNotification } from '../../api/webPush'
|
||||
import { sendUserNotification } from '../../lib/webPush'
|
||||
|
||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
||||
const session = await getServerSession(req, res, getAuthOptions(req))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import serialize from '../api/resolvers/serial.js'
|
||||
import { sendUserNotification } from '../api/webPush/index.js'
|
||||
import { sendUserNotification } from '../lib/webPush.js'
|
||||
import { msatsToSats, numWithUnits } from '../lib/format.js'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { proportions } from '../lib/madness.js'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { sendUserNotification } from '../api/webPush'
|
||||
import { sendUserNotification } from '../lib/webPush'
|
||||
import { FOUND_BLURBS, LOST_BLURBS } from '../lib/constants'
|
||||
|
||||
const STREAK_THRESHOLD = 100
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
getInvoice, getPayment, cancelHodlInvoice, deletePayment,
|
||||
subscribeToInvoices, subscribeToPayments, subscribeToInvoice
|
||||
} from 'ln-service'
|
||||
import { sendUserNotification } from '../api/webPush/index.js'
|
||||
import { sendUserNotification } from '../lib/webPush'
|
||||
import { msatsToSats, numWithUnits } from '../lib/format'
|
||||
import { INVOICE_RETENTION_DAYS } from '../lib/constants'
|
||||
import { datePivot, sleep } from '../lib/time.js'
|
||||
|
|
Loading…
Reference in New Issue