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 @@ -73,8 +73,12 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, - deviceTokens: delivery.androidDeviceTokens, + targetedNotifications: delivery.androidDeviceTokens.map( + deviceToken => ({ + deviceToken, + notification, + }), + ), codeVersion: null, }); } else if (delivery.deviceType === 'ios') { @@ -103,8 +107,10 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, - deviceTokens, + targetedNotifications: deviceTokens.map(deviceToken => ({ + deviceToken, + notification, + })), 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 @@ -41,10 +41,16 @@ import { promiseAll } from 'lib/utils/promises.js'; import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js'; -import { prepareEncryptedIOSNotifications } from './crypto.js'; +import { + prepareEncryptedIOSNotifications, + prepareEncryptedAndroidNotifications, +} from './crypto.js'; import { getAPNsNotificationTopic } from './providers.js'; import { rescindPushNotifs } from './rescind.js'; -import type { TargetedAPNsNotification } from './types.js'; +import type { + TargetedAPNsNotification, + TargetedAndroidNotification, +} from './types.js'; import { apnPush, fcmPush, @@ -238,18 +244,20 @@ platformDetails, ); const deliveryPromise = (async () => { - const notification = await prepareAndroidNotification({ - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID: threadInfo.id, - collapseKey: notifInfo.collapseKey, - badgeOnly, - unreadCount: unreadCounts[userID], - platformDetails, - dbID, - }); - const deviceTokens = devices.map(({ deviceToken }) => deviceToken); - return await sendAndroidNotification(notification, deviceTokens, { + const targetedNotifications = await prepareAndroidNotification( + { + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID: threadInfo.id, + collapseKey: notifInfo.collapseKey, + badgeOnly, + unreadCount: unreadCounts[userID], + platformDetails, + dbID, + }, + devices, + ); + return await sendAndroidNotification(targetedNotifications, { ...notificationInfo, codeVersion, }); @@ -755,7 +763,8 @@ }); async function prepareAndroidNotification( inputData: AndroidNotifInputData, -): Promise { + devices: $ReadOnlyArray, +): Promise<$ReadOnlyArray> { const convertedData = validateOutput( inputData.platformDetails, androidNotifInputDataValidator, @@ -772,6 +781,13 @@ dbID, } = convertedData; + const isTextNotification = newRawMessageInfos.every( + newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, + ); + + const shouldBeEncrypted = + isTextNotification && !collapseKey && codeVersion && codeVersion > 222; + const notifID = collapseKey ? collapseKey : dbID; const { merged, ...rest } = notifTexts; const notification = { @@ -802,22 +818,44 @@ data: { ...notification.data, messageInfos }, }; - if ( - Buffer.byteLength(JSON.stringify(copyWithMessageInfos)) <= - fcmMaxNotificationPayloadByteSize - ) { - return copyWithMessageInfos; - } + const evaluateAndSelectNotification = (notif, notifWithMessageInfos) => { + if ( + Buffer.byteLength(JSON.stringify(notifWithMessageInfos)) <= + fcmMaxNotificationPayloadByteSize + ) { + return notifWithMessageInfos; + } + if ( + Buffer.byteLength(JSON.stringify(notif)) > + fcmMaxNotificationPayloadByteSize + ) { + console.warn( + `Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, + ); + } + return notif; + }; - if ( - Buffer.byteLength(JSON.stringify(notification)) > - fcmMaxNotificationPayloadByteSize - ) { - console.warn( - `Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, - ); + const deviceTokens = devices.map(({ deviceToken }) => deviceToken); + if (shouldBeEncrypted) { + const cookieIDs = devices.map(({ cookieID }) => cookieID); + const [notifications, notificationsWithMessageInfos] = await Promise.all([ + prepareEncryptedAndroidNotifications(cookieIDs, notification), + prepareEncryptedAndroidNotifications(cookieIDs, copyWithMessageInfos), + ]); + return notificationsWithMessageInfos.map((notif, idx) => ({ + notification: evaluateAndSelectNotification(notifications[idx], notif), + deviceToken: deviceTokens[idx], + })); } - return notification; + const notificationToSend = evaluateAndSelectNotification( + notification, + copyWithMessageInfos, + ); + return deviceTokens.map(deviceToken => ({ + notification: notificationToSend, + deviceToken, + })); } type WebNotifInputData = { @@ -975,8 +1013,7 @@ invalidTokens?: $ReadOnlyArray, }; async function sendAndroidNotification( - notification: Object, - deviceTokens: $ReadOnlyArray, + targetedNotifications: $ReadOnlyArray, notificationInfo: NotificationInfo, ): Promise { const collapseKey = notificationInfo.collapseKey @@ -984,11 +1021,13 @@ : null; // for Flow... const { source, codeVersion } = notificationInfo; const response = await fcmPush({ - notification, - deviceTokens, + targetedNotifications, collapseKey, codeVersion, }); + const deviceTokens = targetedNotifications.map( + ({ deviceToken }) => deviceToken, + ); const androidIDs = response.fcmIDs ? response.fcmIDs : []; const delivery: AndroidDelivery = { source, @@ -1234,15 +1273,31 @@ ? { badge: unreadCount.toString() } : { badge: unreadCount.toString(), badgeOnly: '1' }; const notification = { data: notificationData }; - const deviceTokens = deviceInfos.map(({ deviceToken }) => deviceToken); - deliveryPromises.push( - sendAndroidNotification(notification, deviceTokens, { + const deliveryPromise = (async () => { + const cookieIDs = deviceInfos.map(({ cookieID }) => cookieID); + let notificationsArray; + if (codeVersion > 222) { + notificationsArray = await prepareEncryptedAndroidNotifications( + cookieIDs, + notification, + ); + } else { + notificationsArray = cookieIDs.map(() => notification); + } + const targetedNotifications = deviceInfos.map( + ({ deviceToken }, idx) => ({ + deviceToken, + notification: notificationsArray[idx], + }), + ); + return await sendAndroidNotification(targetedNotifications, { source, dbID, userID, codeVersion, - }), - ); + }); + })(); + deliveryPromises.push(deliveryPromise); } } diff --git a/keyserver/src/push/types.js b/keyserver/src/push/types.js --- a/keyserver/src/push/types.js +++ b/keyserver/src/push/types.js @@ -14,3 +14,8 @@ +[string]: string, }, }; + +export type TargetedAndroidNotification = { + +notification: AndroidNotification, + +deviceToken: string, +}; 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 @@ -23,7 +23,10 @@ ensureWebPushInitialized, getWNSToken, } from './providers.js'; -import type { TargetedAPNsNotification } from './types.js'; +import type { + TargetedAPNsNotification, + TargetedAndroidNotification, +} from './types.js'; import { dbQuery, SQL } from '../database/database.js'; const fcmTokenInvalidationErrors = new Set([ @@ -102,13 +105,11 @@ +invalidTokens?: $ReadOnlyArray, }; async function fcmPush({ - notification, - deviceTokens, + targetedNotifications, collapseKey, codeVersion, }: { - +notification: Object, - +deviceTokens: $ReadOnlyArray, + +targetedNotifications: $ReadOnlyArray, +codeVersion: ?number, +collapseKey?: ?string, }): Promise { @@ -131,7 +132,7 @@ // won't explain which of the device tokens is invalid. So we're forced to // avoid the multicast functionality and call it once per deviceToken. const promises = []; - for (const deviceToken of deviceTokens) { + for (const { notification, deviceToken } of targetedNotifications) { promises.push( fcmSinglePush(fcmProvider, notification, deviceToken, options), ); @@ -146,7 +147,7 @@ for (const error of pushResult.errors) { errors.push(error); if (fcmTokenInvalidationErrors.has(error.errorInfo.code)) { - invalidTokens.push(deviceTokens[i]); + invalidTokens.push(targetedNotifications[i].deviceToken); } } for (const id of pushResult.fcmIDs) { diff --git a/keyserver/src/session/cookies.js b/keyserver/src/session/cookies.js --- a/keyserver/src/session/cookies.js +++ b/keyserver/src/session/cookies.js @@ -828,7 +828,8 @@ ): Promise { if ( !viewer.platformDetails || - viewer.platformDetails.platform !== 'ios' || + (viewer.platformDetails.platform !== 'ios' && + viewer.platformDetails.platform !== 'android') || !viewer.platformDetails.codeVersion || viewer.platformDetails.codeVersion < 222 ) {