Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/utils.js
// @flow | // @flow | ||||
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 fetch from 'node-fetch'; | import fetch from 'node-fetch'; | ||||
import type { Response } from 'node-fetch'; | import type { Response } from 'node-fetch'; | ||||
import webpush from 'web-push'; | import webpush from 'web-push'; | ||||
import type { PlatformDetails } from 'lib/types/device-types.js'; | import type { PlatformDetails } from 'lib/types/device-types.js'; | ||||
import type { | import type { | ||||
WebNotification, | WebNotification, | ||||
WNSNotification, | WNSNotification, | ||||
} from 'lib/types/notif-types.js'; | } 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-permission-types.js'; | import { threadPermissions } from 'lib/types/thread-permission-types.js'; | ||||
import { | import { | ||||
getAPNPushProfileForCodeVersion, | getAPNPushProfileForCodeVersion, | ||||
getFCMPushProfileForCodeVersion, | getFCMPushProfileForCodeVersion, | ||||
getAPNProvider, | getAPNProvider, | ||||
getFCMProvider, | getFCMProvider, | ||||
ensureWebPushInitialized, | ensureWebPushInitialized, | ||||
getWNSToken, | getWNSToken, | ||||
} from './providers.js'; | } from './providers.js'; | ||||
import type { TargetedAPNsNotification } from './types.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]; | const webInvalidTokenErrorCodes = [404, 410]; | ||||
const wnsInvalidTokenErrorCodes = [404, 410]; | const wnsInvalidTokenErrorCodes = [404, 410]; | ||||
const wnsMaxNotificationPayloadByteSize = 5000; | const wnsMaxNotificationPayloadByteSize = 5000; | ||||
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({ | ||||
notification, | targetedNotifications, | ||||
deviceTokens, | |||||
platformDetails, | platformDetails, | ||||
}: { | }: { | ||||
+notification: apn.Notification, | +targetedNotifications: $ReadOnlyArray<TargetedAPNsNotification>, | ||||
+deviceTokens: $ReadOnlyArray<string>, | |||||
+platformDetails: PlatformDetails, | +platformDetails: PlatformDetails, | ||||
}): Promise<APNPushResult> { | }): Promise<APNPushResult> { | ||||
const pushProfile = getAPNPushProfileForCodeVersion(platformDetails); | const pushProfile = getAPNPushProfileForCodeVersion(platformDetails); | ||||
const apnProvider = await getAPNProvider(pushProfile); | const apnProvider = await getAPNProvider(pushProfile); | ||||
if (!apnProvider && process.env.NODE_ENV === 'development') { | if (!apnProvider && process.env.NODE_ENV === 'development') { | ||||
console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`); | console.log(`no keyserver/secrets/${pushProfile}.json so ignoring notifs`); | ||||
return { success: true }; | return { success: true }; | ||||
} | } | ||||
invariant(apnProvider, `keyserver/secrets/${pushProfile}.json should exist`); | invariant(apnProvider, `keyserver/secrets/${pushProfile}.json should exist`); | ||||
const result = await apnProvider.send(notification, deviceTokens); | |||||
const results = await Promise.all( | |||||
targetedNotifications.map(({ notification, deviceToken }) => { | |||||
return apnProvider.send(notification, deviceToken); | |||||
}), | |||||
); | |||||
const mergedResults = { sent: [], failed: [] }; | |||||
for (const result of results) { | |||||
mergedResults.sent.push(...result.sent); | |||||
mergedResults.failed.push(...result.failed); | |||||
} | |||||
const errors = []; | const errors = []; | ||||
const invalidTokens = []; | const invalidTokens = []; | ||||
for (const error of result.failed) { | for (const error of mergedResults.failed) { | ||||
errors.push(error); | errors.push(error); | ||||
/* eslint-disable eqeqeq */ | /* eslint-disable eqeqeq */ | ||||
if ( | if ( | ||||
error.status == apnTokenInvalidationErrorCode || | error.status == apnTokenInvalidationErrorCode || | ||||
(error.status == apnBadRequestErrorCode && | (error.status == apnBadRequestErrorCode && | ||||
error.response.reason === apnBadTokenErrorString) | error.response.reason === apnBadTokenErrorString) | ||||
) { | ) { | ||||
invalidTokens.push(error.device); | invalidTokens.push(error.device); | ||||
▲ Show 20 Lines • Show All 300 Lines • Show Last 20 Lines |