UBERF-9230: Fix ses webpush (#7760)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-01-22 19:25:49 +07:00 committed by GitHub
parent 82a9204076
commit 90e8ca4e97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 32 deletions

View File

@ -15,6 +15,7 @@
// //
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import chunter, { ChatMessage } from '@hcengineering/chunter' import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, { import contact, {
Employee, Employee,
@ -39,7 +40,6 @@ import core, {
generateId, generateId,
MeasureContext, MeasureContext,
MixinUpdate, MixinUpdate,
RateLimiter,
Ref, Ref,
RefTo, RefTo,
SortingOrder, SortingOrder,
@ -82,7 +82,6 @@ import serverView from '@hcengineering/server-view'
import { markupToText, stripTags } from '@hcengineering/text-core' import { markupToText, stripTags } from '@hcengineering/text-core'
import { encodeObjectURI } from '@hcengineering/view' import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { Analytics } from '@hcengineering/analytics'
import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types' import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types'
import { import {
@ -92,6 +91,7 @@ import {
getNotificationContent, getNotificationContent,
getNotificationLink, getNotificationLink,
getNotificationProviderControl, getNotificationProviderControl,
getObjectSpace,
getTextPresenter, getTextPresenter,
getUsersInfo, getUsersInfo,
isAllowed, isAllowed,
@ -103,8 +103,7 @@ import {
replaceAll, replaceAll,
toReceiverInfo, toReceiverInfo,
updateNotifyContextsSpace, updateNotifyContextsSpace,
type NotificationProviderControl, type NotificationProviderControl
getObjectSpace
} from './utils' } from './utils'
export function getPushCollaboratorTx ( export function getPushCollaboratorTx (
@ -602,13 +601,7 @@ export async function createPushNotification (
} }
} }
const limiter = new RateLimiter(5) void sendPushToSubscription(sesURL, sesAuth, control, target, userSubscriptions, data)
for (const subscription of userSubscriptions) {
await limiter.add(async () => {
await sendPushToSubscription(sesURL, sesAuth, control, target, subscription, data)
})
}
await limiter.waitProcessing()
} }
async function sendPushToSubscription ( async function sendPushToSubscription (
@ -616,11 +609,11 @@ async function sendPushToSubscription (
sesAuth: string | undefined, sesAuth: string | undefined,
control: TriggerControl, control: TriggerControl,
targetUser: Ref<Account>, targetUser: Ref<Account>,
subscription: PushSubscription, subscriptions: PushSubscription[],
data: PushData data: PushData
): Promise<void> { ): Promise<void> {
try { try {
const result: 'ok' | 'clear-push' = ( const result: Ref<PushSubscription>[] = (
await ( await (
await fetch(concatLink(sesURL, '/web-push'), { await fetch(concatLink(sesURL, '/web-push'), {
method: 'post', method: 'post',
@ -629,15 +622,17 @@ async function sendPushToSubscription (
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {}) ...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
}, },
body: JSON.stringify({ body: JSON.stringify({
subscription, subscriptions,
data data
}) })
}) })
).json() ).json()
).result ).result
if (result === 'clear-push') { if (result.length > 0) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id) const domain = control.hierarchy.findDomain(notification.class.PushSubscription)
await control.apply(control.ctx, [tx]) if (domain !== undefined) {
await control.lowLevel.clean(control.ctx, domain, result)
}
} }
} catch (err) { } catch (err) {
control.ctx.info('Cannot send push notification to', { user: targetUser, err }) control.ctx.info('Cannot send push notification to', { user: targetUser, err })

View File

@ -15,4 +15,8 @@
import { main } from './main' import { main } from './main'
void main() void main().catch((err) => {
if (err != null) {
console.error(err)
}
})

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Ref } from '@hcengineering/core'
import { PushSubscription, type PushData } from '@hcengineering/notification' import { PushSubscription, type PushData } from '@hcengineering/notification'
import type { Request, Response } from 'express' import type { Request, Response } from 'express'
import webpush, { WebPushError } from 'web-push' import webpush, { WebPushError } from 'web-push'
@ -22,25 +23,39 @@ import { SES } from './ses'
import { Endpoint } from './types' import { Endpoint } from './types'
const errorMessages = ['expired', 'Unregistered', 'No such subscription'] const errorMessages = ['expired', 'Unregistered', 'No such subscription']
async function sendPushToSubscription (subscription: PushSubscription, data: PushData): Promise<'ok' | 'clear-push'> { async function sendPushToSubscription (
subscriptions: PushSubscription[],
data: PushData
): Promise<Ref<PushSubscription>[]> {
const result: Ref<PushSubscription>[] = []
for (const subscription of subscriptions) {
try { try {
await webpush.sendNotification(subscription, JSON.stringify(data)) await webpush.sendNotification(subscription, JSON.stringify(data))
} catch (err: any) { } catch (err: any) {
if (err instanceof WebPushError) { if (err instanceof WebPushError) {
if (errorMessages.some((p) => JSON.stringify((err as WebPushError).body).includes(p))) { if (errorMessages.some((p) => JSON.stringify((err as WebPushError).body).includes(p))) {
return 'clear-push' result.push(subscription._id)
} }
} }
} }
return 'ok' }
return result
} }
export const main = async (): Promise<void> => { export const main = async (): Promise<void> => {
const ses = new SES() const ses = new SES()
console.log('SES service has been started') console.log('SES service has been started')
let webpushInitDone = false
if (config.PushPublicKey !== undefined && config.PushPrivateKey !== undefined) { if (config.PushPublicKey !== undefined && config.PushPrivateKey !== undefined) {
webpush.setVapidDetails(config.PushSubject ?? 'mailto:hey@huly.io', config.PushPublicKey, config.PushPublicKey) try {
const subj = config.PushSubject ?? 'mailto:hey@huly.io'
console.log('Setting VAPID details', subj, config.PushPublicKey.length, config.PushPrivateKey.length)
webpush.setVapidDetails(config.PushSubject ?? 'mailto:hey@huly.io', config.PushPublicKey, config.PushPrivateKey)
webpushInitDone = true
} catch (err: any) {
console.error(err)
}
} }
const checkAuth = (req: Request<any>, res: Response<any>): boolean => { const checkAuth = (req: Request<any>, res: Response<any>): boolean => {
@ -104,14 +119,18 @@ export const main = async (): Promise<void> => {
res.status(400).send({ err: "'data' is missing" }) res.status(400).send({ err: "'data' is missing" })
return return
} }
const subscription: PushSubscription | undefined = req.body?.subscription const subscriptions: PushSubscription[] | undefined = req.body?.subscriptions
if (subscription === undefined) { if (subscriptions === undefined) {
res.status(400).send({ err: "'subscription' is missing" }) res.status(400).send({ err: "'subscriptions' is missing" })
return
}
if (!webpushInitDone) {
res.json({ result: [] }).end()
return return
} }
const result = await sendPushToSubscription(subscription, data) const result = await sendPushToSubscription(subscriptions, data)
res.json({ result }) res.json({ result }).end()
} }
} }
] ]