Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/utils.js
// @flow | // @flow | ||||
import apn from '@parse/node-apn'; | import apn from '@parse/node-apn'; | ||||
import type { ResponseFailure } from '@parse/node-apn'; | import type { ResponseFailure } from '@parse/node-apn'; | ||||
import type { FirebaseApp, FirebaseError } from 'firebase-admin'; | import type { FirebaseApp, FirebaseError } from 'firebase-admin'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import webpush from 'web-push'; | |||||
import type { WebNotification } from 'lib/types/notif-types.js'; | |||||
import { threadSubscriptions } from 'lib/types/subscription-types.js'; | import { threadSubscriptions } from 'lib/types/subscription-types.js'; | ||||
import { threadPermissions } from 'lib/types/thread-types.js'; | import { threadPermissions } from 'lib/types/thread-types.js'; | ||||
import { | import { | ||||
getAPNPushProfileForCodeVersion, | getAPNPushProfileForCodeVersion, | ||||
getFCMPushProfileForCodeVersion, | getFCMPushProfileForCodeVersion, | ||||
getAPNProvider, | getAPNProvider, | ||||
getFCMProvider, | getFCMProvider, | ||||
ensureWebPushInitialized, | |||||
} from './providers.js'; | } from './providers.js'; | ||||
import { dbQuery, SQL } from '../database/database.js'; | import { dbQuery, SQL } from '../database/database.js'; | ||||
const fcmTokenInvalidationErrors = new Set([ | const fcmTokenInvalidationErrors = new Set([ | ||||
'messaging/registration-token-not-registered', | 'messaging/registration-token-not-registered', | ||||
'messaging/invalid-registration-token', | 'messaging/invalid-registration-token', | ||||
]); | ]); | ||||
const fcmMaxNotificationPayloadByteSize = 4000; | const fcmMaxNotificationPayloadByteSize = 4000; | ||||
const apnTokenInvalidationErrorCode = 410; | const apnTokenInvalidationErrorCode = 410; | ||||
const apnBadRequestErrorCode = 400; | const apnBadRequestErrorCode = 400; | ||||
const apnBadTokenErrorString = 'BadDeviceToken'; | const apnBadTokenErrorString = 'BadDeviceToken'; | ||||
const apnMaxNotificationPayloadByteSize = 4096; | const apnMaxNotificationPayloadByteSize = 4096; | ||||
const webInvalidTokenErrorCodes = [404, 410]; | |||||
type APNPushResult = | type APNPushResult = | ||||
| { +success: true } | | { +success: true } | ||||
| { | | { | ||||
+errors: $ReadOnlyArray<ResponseFailure>, | +errors: $ReadOnlyArray<ResponseFailure>, | ||||
+invalidTokens?: $ReadOnlyArray<string>, | +invalidTokens?: $ReadOnlyArray<string>, | ||||
}; | }; | ||||
async function apnPush({ | async function apnPush({ | ||||
▲ Show 20 Lines • Show All 157 Lines • ▼ Show 20 Lines | ): Promise<{ [userID: string]: number }> { | ||||
for (const userID of userIDs) { | for (const userID of userIDs) { | ||||
if (usersToUnreadCounts[userID] === undefined) { | if (usersToUnreadCounts[userID] === undefined) { | ||||
usersToUnreadCounts[userID] = 0; | usersToUnreadCounts[userID] = 0; | ||||
} | } | ||||
} | } | ||||
return usersToUnreadCounts; | return usersToUnreadCounts; | ||||
} | } | ||||
export type WebPushError = { | |||||
+statusCode: number, | |||||
+headers: { +[string]: string }, | |||||
+body: string, | |||||
}; | |||||
type WebPushResult = { | |||||
+success?: true, | |||||
+errors?: $ReadOnlyArray<WebPushError>, | |||||
+invalidTokens?: $ReadOnlyArray<string>, | |||||
}; | |||||
async function webPush({ | |||||
notification, | |||||
deviceTokens, | |||||
}: { | |||||
+notification: WebNotification, | |||||
+deviceTokens: $ReadOnlyArray<string>, | |||||
}): Promise<WebPushResult> { | |||||
await ensureWebPushInitialized(); | |||||
const notificationString = JSON.stringify(notification); | |||||
const pushResults = await Promise.all( | |||||
deviceTokens.map(async deviceTokenString => { | |||||
const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString); | |||||
try { | |||||
await webpush.sendNotification(deviceToken, notificationString); | |||||
} catch (error) { | |||||
return { error }; | |||||
} | |||||
return {}; | |||||
}), | |||||
); | |||||
const errors = []; | |||||
const invalidTokens = []; | |||||
for (let i = 0; i < pushResults.length; i++) { | |||||
const pushResult = pushResults[i]; | |||||
if (pushResult.error) { | |||||
errors.push(pushResult.error); | |||||
if (webInvalidTokenErrorCodes.includes(pushResult.error.statusCode)) { | |||||
invalidTokens.push(deviceTokens[i]); | |||||
} | |||||
} | |||||
} | |||||
const result = {}; | |||||
if (errors.length > 0) { | |||||
result.errors = errors; | |||||
} else { | |||||
result.success = true; | |||||
} | |||||
if (invalidTokens.length > 0) { | |||||
result.invalidTokens = invalidTokens; | |||||
} | |||||
return { ...result }; | |||||
} | |||||
export { | export { | ||||
apnPush, | apnPush, | ||||
fcmPush, | fcmPush, | ||||
webPush, | |||||
getUnreadCounts, | getUnreadCounts, | ||||
apnMaxNotificationPayloadByteSize, | apnMaxNotificationPayloadByteSize, | ||||
fcmMaxNotificationPayloadByteSize, | fcmMaxNotificationPayloadByteSize, | ||||
}; | }; |