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,7 +69,7 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, + notifications: [notification], deviceTokens: delivery.androidDeviceTokens, codeVersion: null, }); @@ -98,7 +98,7 @@ threadID, ); deliveryPromises[id] = fcmPush({ - notification, + notifications: [notification], deviceTokens, 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,9 +41,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, @@ -235,25 +239,31 @@ } 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, - }); + const notificationsArray = await prepareAndroidNotification( + { + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID: threadInfo.id, + collapseKey: notifInfo.collapseKey, + badgeOnly, + unreadCount: unreadCounts[userID], + platformDetails, + dbID, + }, + [...cookieIDs], + ); return await sendAndroidNotification( - notification, + notificationsArray, [...deviceTokens], { ...notificationInfo, @@ -755,7 +765,8 @@ }); async function prepareAndroidNotification( inputData: AndroidNotifInputData, -): Promise { + cookieIDs?: $ReadOnlyArray, +): Promise> { const convertedData = validateOutput( inputData.platformDetails, androidNotifInputDataValidator, @@ -772,6 +783,13 @@ dbID, } = convertedData; + const isTextNotification = newRawMessageInfos.every( + newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, + ); + + const shouldBeEncrypted = + isTextNotification && !collapseKey && codeVersion && codeVersion > 0; + const notifID = collapseKey ? collapseKey : dbID; const { merged, ...rest } = notifTexts; const notification = { @@ -802,22 +820,43 @@ data: { ...notification.data, messageInfos }, }; - if ( - Buffer.byteLength(JSON.stringify(copyWithMessageInfos)) <= - fcmMaxNotificationPayloadByteSize - ) { - return copyWithMessageInfos; + let notifications, notificationsWithMessageInfos; + if (shouldBeEncrypted && cookieIDs) { + [notifications, notificationsWithMessageInfos] = await Promise.all([ + prepareEncryptedAndroidNotifications(cookieIDs, notification), + prepareEncryptedAndroidNotifications(cookieIDs, copyWithMessageInfos), + ]); + } else { + notifications = [notification]; + notificationsWithMessageInfos = [copyWithMessageInfos]; } - if ( - Buffer.byteLength(JSON.stringify(notification)) > - fcmMaxNotificationPayloadByteSize - ) { - console.warn( - `Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, - ); + const shouldAddMessageInfos = notificationsWithMessageInfos.map( + notif => + Buffer.byteLength(JSON.stringify(notif)) <= + fcmMaxNotificationPayloadByteSize, + ); + + const notificationsToSend = shouldAddMessageInfos.map( + (addMessageInfos, idx) => { + if (addMessageInfos) { + return notificationsWithMessageInfos[idx]; + } + return notifications[idx]; + }, + ); + + for (const notif of notificationsToSend) { + if ( + Buffer.byteLength(JSON.stringify(notif)) > + fcmMaxNotificationPayloadByteSize + ) { + console.warn( + `Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, + ); + } } - return notification; + return notificationsToSend; } type WebNotifInputData = { @@ -969,7 +1008,7 @@ invalidTokens?: $ReadOnlyArray, }; async function sendAndroidNotification( - notification: Object, + notifications: $ReadOnlyArray, deviceTokens: $ReadOnlyArray, notificationInfo: NotificationInfo, ): Promise { @@ -978,7 +1017,7 @@ : null; // for Flow... const { source, codeVersion } = notificationInfo; const response = await fcmPush({ - notification, + notifications, deviceTokens, collapseKey, codeVersion, @@ -1210,14 +1249,24 @@ 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 > 0 && cookieIDs + ? await prepareEncryptedAndroidNotifications( + [...cookieIDs], + notification, + ) + : [notification]; deliveryPromises.push( - sendAndroidNotification(notification, [...deviceTokens], { + sendAndroidNotification(notificationsArray, [...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 @@ -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,12 +105,12 @@ +invalidTokens?: $ReadOnlyArray, }; async function fcmPush({ - notification, + notifications, deviceTokens, collapseKey, codeVersion, }: { - +notification: Object, + +notifications: $ReadOnlyArray, +deviceTokens: $ReadOnlyArray, +codeVersion: ?number, +collapseKey?: ?string, @@ -133,7 +134,9 @@ // 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 [idx, deviceToken] of deviceTokens.entries()) { + const notification = + idx < notifications.length ? notifications[idx] : notifications[0]; promises.push( fcmSinglePush(fcmProvider, notification, deviceToken, options), ); 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,9 +828,10 @@ ): Promise { if ( !viewer.platformDetails || - viewer.platformDetails.platform !== 'ios' || + (viewer.platformDetails.platform !== 'ios' && + viewer.platformDetails.platform !== 'android') || !viewer.platformDetails.codeVersion || - viewer.platformDetails.codeVersion < 1000 + viewer.platformDetails.codeVersion < 10 ) { return false; }