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 @@ -69,8 +69,11 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, - deviceTokens: delivery.androidDeviceTokens, + notificationDeviceTokenPairs: delivery.androidDeviceTokens.map( + deviceToken => { + deviceToken, notification; + }, + ), codeVersion: null, }); } else if (delivery.deviceType === 'ios') { @@ -98,8 +101,9 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, - deviceTokens, + notificationDeviceTokenPairs: 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 @@ -42,9 +42,13 @@ 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 { AndroidNotification } from './types.js'; import { apnPush, fcmPush, @@ -236,31 +240,38 @@ } const androidVersionsToTokens = byPlatform.get('android'); if (androidVersionsToTokens) { - for (const [codeVersion, { deviceTokens }] of androidVersionsToTokens) { + for (const [ + codeVersion, + { cookieIDs, deviceTokens }, + ] of androidVersionsToTokens) { const platformDetails = { platform: 'android', codeVersion }; const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ); const deliveryPromise = (async () => { - const notification = await prepareAndroidNotification({ - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID: threadInfo.id, - collapseKey: notifInfo.collapseKey, - badgeOnly, - unreadCount: unreadCounts[userID], - platformDetails, - dbID, - }); - return await sendAndroidNotification( - notification, - [...deviceTokens], + const notificationsArray = await prepareAndroidNotification( { - ...notificationInfo, - codeVersion, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID: threadInfo.id, + collapseKey: notifInfo.collapseKey, + badgeOnly, + unreadCount: unreadCounts[userID], + platformDetails, + dbID, + }, + [...cookieIDs], + ); + const notificationDeviceTokenPairs = [...deviceTokens].map( + (deviceToken, idx) => { + return { deviceToken, notification: notificationsArray[idx] }; }, ); + return await sendAndroidNotification(notificationDeviceTokenPairs, { + ...notificationInfo, + codeVersion, + }); })(); deliveryPromises.push(deliveryPromise); } @@ -758,7 +769,8 @@ }); async function prepareAndroidNotification( inputData: AndroidNotifInputData, -): Promise { + cookieIDs: $ReadOnlyArray, +): Promise> { const convertedData = validateOutput( inputData.platformDetails, androidNotifInputDataValidator, @@ -775,6 +787,16 @@ dbID, } = convertedData; + const isTextNotification = newRawMessageInfos.every( + newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, + ); + + const shouldBeEncrypted = + isTextNotification && + !collapseKey && + codeVersion && + codeVersion > FUTURE_CODE_VERSION; + const notifID = collapseKey ? collapseKey : dbID; const { merged, ...rest } = notifTexts; const notification = { @@ -805,22 +827,38 @@ 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`, + if (shouldBeEncrypted) { + const [notifications, notificationsWithMessageInfos] = await Promise.all([ + prepareEncryptedAndroidNotifications(cookieIDs, notification), + prepareEncryptedAndroidNotifications(cookieIDs, copyWithMessageInfos), + ]); + return notificationsWithMessageInfos.map((notif, idx) => + evaluateAndSelectNotification(notifications[idx], notif), ); } - return notification; + const notificationToSend = evaluateAndSelectNotification( + notification, + copyWithMessageInfos, + ); + return cookieIDs.map(() => notificationToSend); } type WebNotifInputData = { @@ -976,8 +1014,10 @@ invalidTokens?: $ReadOnlyArray, }; async function sendAndroidNotification( - notification: Object, - deviceTokens: $ReadOnlyArray, + notificationDeviceTokenPairs: $ReadOnlyArray<{ + +notification: AndroidNotification, + +deviceToken: string, + }>, notificationInfo: NotificationInfo, ): Promise { const collapseKey = notificationInfo.collapseKey @@ -985,11 +1025,13 @@ : null; // for Flow... const { source, codeVersion } = notificationInfo; const response = await fcmPush({ - notification, - deviceTokens, + notificationDeviceTokenPairs, collapseKey, codeVersion, }); + const deviceTokens = notificationDeviceTokenPairs.map( + pair => pair.deviceToken, + ); const androidIDs = response.fcmIDs ? response.fcmIDs : []; const delivery: AndroidDelivery = { source, @@ -1217,14 +1259,29 @@ const androidVersionsToTokens = byPlatform.get('android'); if (androidVersionsToTokens) { - for (const [codeVersion, { deviceTokens }] of androidVersionsToTokens) { + for (const [ + codeVersion, + { cookieIDs, deviceTokens }, + ] of androidVersionsToTokens) { const notificationData = codeVersion < 69 ? { badge: unreadCount.toString() } : { badge: unreadCount.toString(), badgeOnly: '1' }; const notification = { data: notificationData }; + const notificationsArray = + codeVersion > FUTURE_CODE_VERSION + ? await prepareEncryptedAndroidNotifications( + [...cookieIDs], + notification, + ) + : [...cookieIDs].map(() => notification); + const notificationDeviceTokenPairs = [...deviceTokens].map( + (deviceToken, idx) => { + return { deviceToken, notification: notificationsArray[idx] }; + }, + ); deliveryPromises.push( - sendAndroidNotification(notification, [...deviceTokens], { + sendAndroidNotification(notificationDeviceTokenPairs, { 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 @@ -24,6 +24,7 @@ ensureWebPushInitialized, getWNSToken, } from './providers.js'; +import type { AndroidNotification } from './types.js'; import { dbQuery, SQL } from '../database/database.js'; const fcmTokenInvalidationErrors = new Set([ @@ -104,13 +105,14 @@ +invalidTokens?: $ReadOnlyArray, }; async function fcmPush({ - notification, - deviceTokens, + notificationDeviceTokenPairs, collapseKey, codeVersion, }: { - +notification: Object, - +deviceTokens: $ReadOnlyArray, + +notificationDeviceTokenPairs: $ReadOnlyArray<{ + +notification: AndroidNotification, + +deviceToken: string, + }>, +codeVersion: ?number, +collapseKey?: ?string, }): Promise { @@ -133,7 +135,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 notificationDeviceTokenPairs) { promises.push( fcmSinglePush(fcmProvider, notification, deviceToken, options), ); @@ -148,7 +150,7 @@ for (const error of pushResult.errors) { errors.push(error); if (fcmTokenInvalidationErrors.has(error.errorInfo.code)) { - invalidTokens.push(deviceTokens[i]); + invalidTokens.push(notificationDeviceTokenPairs[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 @@ -831,7 +831,8 @@ ): Promise { if ( !viewer.platformDetails || - viewer.platformDetails.platform !== 'ios' || + (viewer.platformDetails.platform !== 'ios' && + viewer.platformDetails.platform !== 'android') || !viewer.platformDetails.codeVersion || viewer.platformDetails.codeVersion < FUTURE_CODE_VERSION ) {