diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js --- a/keyserver/src/creators/message-creator.js +++ b/keyserver/src/creators/message-creator.js @@ -52,6 +52,7 @@ { +platform: string, +deviceToken: string, + +cookieID: string, +codeVersion: ?string, }, >, @@ -323,7 +324,7 @@ const time = earliestFocusedTimeConsideredExpired(); const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`; const query = SQL` - SELECT m.user, m.thread, c.platform, c.device_token, c.versions, + SELECT m.user, m.thread, c.platform, c.device_token, c.versions, c.id, f.user AS focused_user `; query.append(subthreadSelects); @@ -349,6 +350,7 @@ const focusedUser = !!row.focused_user; const { platform } = row; const versions = JSON.parse(row.versions); + const cookieID = row.id; let thisUserInfo = perUserInfo.get(userID); if (!thisUserInfo) { thisUserInfo = { @@ -384,10 +386,11 @@ } } } - if (deviceToken) { + if (deviceToken && cookieID) { thisUserInfo.devices.set(deviceToken, { platform, deviceToken, + cookieID: cookieID.toString(), codeVersion: versions ? versions.codeVersion : null, }); } diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js --- a/keyserver/src/push/rescind.js +++ b/keyserver/src/push/rescind.js @@ -53,13 +53,11 @@ for (const delivery of deliveries) { if (delivery.iosID && delivery.iosDeviceTokens) { // Old iOS - const notification = prepareIOSNotification( - delivery.iosID, - row.unread_count, - threadID, - ); + const notifications = [ + prepareIOSNotification(delivery.iosID, row.unread_count, threadID), + ]; deliveryPromises[id] = apnPush({ - notification, + notifications, deviceTokens: delivery.iosDeviceTokens, platformDetails: { platform: 'ios' }, }); @@ -78,14 +76,16 @@ } else if (delivery.deviceType === 'ios') { // New iOS const { iosID, deviceTokens, codeVersion } = delivery; - const notification = prepareIOSNotification( - iosID, - row.unread_count, - threadID, - codeVersion, - ); + const notifications = [ + prepareIOSNotification( + iosID, + row.unread_count, + threadID, + codeVersion, + ), + ]; deliveryPromises[id] = apnPush({ - notification, + notifications, deviceTokens, platformDetails: { platform: 'ios', codeVersion }, }); diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -37,6 +37,7 @@ import { updateTypes } from 'lib/types/update-types-enum.js'; import { promiseAll } from 'lib/utils/promises.js'; +import { prepareEncryptedIOSNotifications } from './crypto.js'; import { getAPNsNotificationTopic } from './providers.js'; import { rescindPushNotifs } from './rescind.js'; import { @@ -64,6 +65,7 @@ type Device = { +platform: Platform, +deviceToken: string, + +cookieID: string, +codeVersion: ?number, }; type PushUserInfo = { @@ -190,25 +192,32 @@ const iosVersionsToTokens = byPlatform.get('ios'); if (iosVersionsToTokens) { - for (const [codeVersion, deviceTokens] of iosVersionsToTokens) { + for (const [ + codeVersion, + { cookieIDs, deviceTokens }, + ] of iosVersionsToTokens) { const platformDetails = { platform: 'ios', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ); + const deliveryPromise = (async () => { - const notification = await prepareAPNsNotification({ - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID: threadInfo.id, - collapseKey: notifInfo.collapseKey, - badgeOnly, - unreadCount: unreadCounts[userID], - platformDetails, - }); + const notificationsArray = await prepareAPNsNotification( + { + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID: threadInfo.id, + collapseKey: notifInfo.collapseKey, + badgeOnly, + unreadCount: unreadCounts[userID], + platformDetails, + }, + [...cookieIDs], + ); return await sendAPNsNotification( 'ios', - notification, + notificationsArray, [...deviceTokens], { ...notificationInfo, @@ -221,7 +230,7 @@ } const androidVersionsToTokens = byPlatform.get('android'); if (androidVersionsToTokens) { - for (const [codeVersion, deviceTokens] of androidVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of androidVersionsToTokens) { const platformDetails = { platform: 'android', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, @@ -252,7 +261,7 @@ } const webVersionsToTokens = byPlatform.get('web'); if (webVersionsToTokens) { - for (const [codeVersion, deviceTokens] of webVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of webVersionsToTokens) { const deliveryPromise = (async () => { const notification = await prepareWebNotification({ notifTexts, @@ -269,7 +278,7 @@ } const macosVersionsToTokens = byPlatform.get('macos'); if (macosVersionsToTokens) { - for (const [codeVersion, deviceTokens] of macosVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of macosVersionsToTokens) { const platformDetails = { platform: 'macos', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, @@ -300,7 +309,7 @@ } const windowsVersionsToTokens = byPlatform.get('windows'); if (windowsVersionsToTokens) { - for (const [codeVersion, deviceTokens] of windowsVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of windowsVersionsToTokens) { const deliveryPromise = (async () => { const notification = await prepareWNSNotification({ notifTexts, @@ -577,8 +586,11 @@ } function getDevicesByPlatform( - devices: Device[], -): Map>> { + devices: $ReadOnlyArray, +): Map< + Platform, + Map, deviceTokens: Set }>, +> { const byPlatform = new Map(); for (const device of devices) { let innerMap = byPlatform.get(device.platform); @@ -590,12 +602,14 @@ device.codeVersion !== null && device.codeVersion !== undefined ? device.codeVersion : -1; - let innerMostSet = innerMap.get(codeVersion); - if (!innerMostSet) { - innerMostSet = new Set(); - innerMap.set(codeVersion, innerMostSet); + let innerMostPair = innerMap.get(codeVersion); + if (!innerMostPair) { + innerMostPair = { cookieIDs: new Set(), deviceTokens: new Set() }; + innerMap.set(codeVersion, innerMostPair); } - innerMostSet.add(device.deviceToken); + const { cookieIDs, deviceTokens } = innerMostPair; + cookieIDs.add(device.cookieID); + deviceTokens.add(device.deviceToken); } return byPlatform; } @@ -611,7 +625,8 @@ }; async function prepareAPNsNotification( inputData: APNsNotifInputData, -): Promise { + cookieIDs?: $ReadOnlyArray, +): Promise> { const { notifTexts, newRawMessageInfos, @@ -622,6 +637,12 @@ platformDetails, } = inputData; + const isTextNotification = newRawMessageInfos.every( + newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, + ); + const shouldBeEncrypted = + platformDetails.platform === 'ios' && !collapseKey && isTextNotification; + const uniqueID = uuidv4(); const notification = new apn.Notification(); notification.topic = getAPNsNotificationTopic(platformDetails); @@ -662,18 +683,43 @@ ...copyWithMessageInfos.payload, messageInfos, }; - if (copyWithMessageInfos.length() <= apnMaxNotificationPayloadByteSize) { - notification.payload.messageInfos = messageInfos; - return notification; + + let notifications, notificationsWithMessageInfos; + if (shouldBeEncrypted && cookieIDs) { + [notifications, notificationsWithMessageInfos] = await Promise.all([ + prepareEncryptedIOSNotifications(cookieIDs, notification), + prepareEncryptedIOSNotifications(cookieIDs, copyWithMessageInfos), + ]); + } else { + notifications = [notification]; + notificationsWithMessageInfos = [copyWithMessageInfos]; } - const notificationCopy = _cloneDeep(notification); - if (notificationCopy.length() > apnMaxNotificationPayloadByteSize) { - console.warn( - `${platformDetails.platform} notification ${uniqueID} ` + - `exceeds size limit, even with messageInfos omitted`, - ); + + const shouldAddMessageInfos = notificationsWithMessageInfos.map(notif => { + const copy = _cloneDeep(notif); + return copy.length() <= apnMaxNotificationPayloadByteSize; + }); + + const notificationsToSend = shouldAddMessageInfos.map( + (addMessageInfos, idx) => { + if (addMessageInfos) { + return notificationsWithMessageInfos[idx]; + } + return notifications[idx]; + }, + ); + + for (const notifToSend of notificationsToSend) { + const copy = _cloneDeep(notifToSend); + if (copy.length() > apnMaxNotificationPayloadByteSize) { + console.warn( + `${platformDetails.platform} notification ${uniqueID} ` + + `exceeds size limit, even with messageInfos omitted`, + ); + } } - return notification; + + return notificationsToSend; } type AndroidNotifInputData = { @@ -819,16 +865,18 @@ }; async function sendAPNsNotification( platform: 'ios' | 'macos', - notification: apn.Notification, + notifications: $ReadOnlyArray, deviceTokens: $ReadOnlyArray, notificationInfo: NotificationInfo, ): Promise { const { source, codeVersion } = notificationInfo; + const response = await apnPush({ - notification, + notifications, deviceTokens, platformDetails: { platform, codeVersion }, }); + const [notification] = notifications; const delivery: APNsDelivery = { source, deviceType: platform, @@ -1078,7 +1126,10 @@ const iosVersionsToTokens = byPlatform.get('ios'); if (iosVersionsToTokens) { - for (const [codeVersion, deviceTokens] of iosVersionsToTokens) { + for (const [ + codeVersion, + { cookieIDs, deviceTokens }, + ] of iosVersionsToTokens) { const notification = new apn.Notification(); notification.topic = getAPNsNotificationTopic({ platform: 'ios', @@ -1086,8 +1137,12 @@ }); notification.badge = unreadCount; notification.pushType = 'alert'; + const notificationsArray = await prepareEncryptedIOSNotifications( + [...cookieIDs], + notification, + ); deliveryPromises.push( - sendAPNsNotification('ios', notification, [...deviceTokens], { + sendAPNsNotification('ios', notificationsArray, [...deviceTokens], { source, dbID, userID, @@ -1099,7 +1154,7 @@ const androidVersionsToTokens = byPlatform.get('android'); if (androidVersionsToTokens) { - for (const [codeVersion, deviceTokens] of androidVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of androidVersionsToTokens) { const notificationData = codeVersion < 69 ? { badge: unreadCount.toString() } @@ -1118,7 +1173,7 @@ const macosVersionsToTokens = byPlatform.get('macos'); if (macosVersionsToTokens) { - for (const [codeVersion, deviceTokens] of macosVersionsToTokens) { + for (const [codeVersion, { deviceTokens }] of macosVersionsToTokens) { const notification = new apn.Notification(); notification.topic = getAPNsNotificationTopic({ platform: 'macos', @@ -1127,7 +1182,7 @@ notification.badge = unreadCount; notification.pushType = 'alert'; deliveryPromises.push( - sendAPNsNotification('macos', notification, [...deviceTokens], { + sendAPNsNotification('macos', [notification], [...deviceTokens], { source, dbID, userID, diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js --- a/keyserver/src/push/utils.js +++ b/keyserver/src/push/utils.js @@ -46,11 +46,11 @@ +invalidTokens?: $ReadOnlyArray, }; async function apnPush({ - notification, + notifications, deviceTokens, platformDetails, }: { - +notification: apn.Notification, + +notifications: $ReadOnlyArray, +deviceTokens: $ReadOnlyArray, +platformDetails: PlatformDetails, }): Promise { @@ -61,10 +61,22 @@ return { success: true }; } invariant(apnProvider, `keyserver/secrets/${pushProfile}.json should exist`); - const result = await apnProvider.send(notification, deviceTokens); + + const results = await Promise.all( + notifications.map((notification, idx) => { + return apnProvider.send(notification, deviceTokens[idx]); + }), + ); + + const mergedResults = { sent: [], failed: [] }; + for (const result of results) { + mergedResults.sent.push(...result.sent); + mergedResults.failed.push(...result.failed); + } + const errors = []; const invalidTokens = []; - for (const error of result.failed) { + for (const error of mergedResults.failed) { errors.push(error); /* eslint-disable eqeqeq */ if (