diff --git a/lib/push/android-notif-creators.js b/lib/push/android-notif-creators.js index 38e151f9c..7ab2aae3c 100644 --- a/lib/push/android-notif-creators.js +++ b/lib/push/android-notif-creators.js @@ -1,407 +1,392 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { prepareEncryptedAndroidVisualNotifications, prepareEncryptedAndroidSilentNotifications, } from './crypto.js'; -import { - hasMinCodeVersion, - FUTURE_CODE_VERSION, -} from '../shared/version-utils.js'; +import { hasMinCodeVersion } from '../shared/version-utils.js'; import type { PlatformDetails } from '../types/device-types.js'; import { messageTypes } from '../types/message-types-enum.js'; import { type RawMessageInfo, rawMessageInfoValidator, } from '../types/message-types.js'; import { type AndroidVisualNotification, type NotificationTargetDevice, type TargetedAndroidNotification, type ResolvedNotifTexts, resolvedNotifTextsValidator, type SenderDeviceDescriptor, senderDeviceDescriptorValidator, type EncryptedNotifUtilsAPI, type AndroidBadgeOnlyNotification, } from '../types/notif-types.js'; import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js'; export const fcmMaxNotificationPayloadByteSize = 4000; export type CommonNativeNotifInputData = $ReadOnly<{ +senderDeviceDescriptor: SenderDeviceDescriptor, +notifTexts: ResolvedNotifTexts, +newRawMessageInfos: $ReadOnlyArray, +threadID: string, +collapseKey: ?string, +badgeOnly: boolean, +unreadCount?: number, +platformDetails: PlatformDetails, }>; export const commonNativeNotifInputDataValidator: TInterface = tShape({ senderDeviceDescriptor: senderDeviceDescriptorValidator, notifTexts: resolvedNotifTextsValidator, newRawMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, collapseKey: t.maybe(t.String), badgeOnly: t.Boolean, unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); export type AndroidNotifInputData = $ReadOnly<{ ...CommonNativeNotifInputData, +notifID: string, }>; export const androidNotifInputDataValidator: TInterface = tShape({ ...commonNativeNotifInputDataValidator.meta.props, notifID: t.String, }); async function createAndroidVisualNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: AndroidNotifInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { senderDeviceDescriptor, notifTexts, newRawMessageInfos, threadID, collapseKey, badgeOnly, unreadCount, platformDetails, notifID, } = inputData; const canDecryptNonCollapsibleTextNotifs = hasMinCodeVersion( platformDetails, { native: 228 }, ); const isNonCollapsibleTextNotif = newRawMessageInfos.every( newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, ) && !collapseKey; const canDecryptAllNotifTypes = hasMinCodeVersion(platformDetails, { native: 267, }); const shouldBeEncrypted = canDecryptAllNotifTypes || (canDecryptNonCollapsibleTextNotifs && isNonCollapsibleTextNotif); const { merged, ...rest } = notifTexts; const notification = { data: { ...rest, threadID, }, }; if (unreadCount !== undefined && unreadCount !== null) { notification.data = { ...notification.data, badge: unreadCount.toString(), }; } let id; if (collapseKey && canDecryptAllNotifTypes) { id = notifID; notification.data = { ...notification.data, collapseKey, }; } else if (collapseKey) { id = collapseKey; } else { id = notifID; } notification.data = { ...notification.data, id, badgeOnly: badgeOnly ? '1' : '0', }; const messageInfos = JSON.stringify(newRawMessageInfos); const copyWithMessageInfos = { ...notification, data: { ...notification.data, messageInfos }, }; const priority = 'high'; if (!shouldBeEncrypted) { const notificationToSend = encryptedNotifUtilsAPI.getNotifByteSize( JSON.stringify(copyWithMessageInfos), ) <= fcmMaxNotificationPayloadByteSize ? copyWithMessageInfos : notification; return devices.map(({ deliveryID }) => ({ priority, notification: notificationToSend, deliveryID, })); } const notificationsSizeValidator = (notif: AndroidVisualNotification) => { const serializedNotif = JSON.stringify(notif); return ( !serializedNotif || encryptedNotifUtilsAPI.getNotifByteSize(serializedNotif) <= fcmMaxNotificationPayloadByteSize ); }; const notifsWithMessageInfos = await prepareEncryptedAndroidVisualNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, copyWithMessageInfos, notificationsSizeValidator, ); const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => payloadSizeExceeded) .map(({ cryptoID, deliveryID }) => ({ cryptoID, deliveryID })); if (devicesWithExcessiveSizeNoHolders.length === 0) { return notifsWithMessageInfos.map( ({ notification: notif, deliveryID, encryptionOrder }) => ({ priority, notification: notif, deliveryID, encryptionOrder, }), ); } const canQueryBlobService = hasMinCodeVersion(platformDetails, { native: 331, }); let blobHash, blobHolders, encryptionKey, blobUploadError; if (canQueryBlobService) { ({ blobHash, blobHolders, encryptionKey, blobUploadError } = await encryptedNotifUtilsAPI.uploadLargeNotifPayload( JSON.stringify(copyWithMessageInfos.data), devicesWithExcessiveSizeNoHolders.length, )); } if (blobUploadError) { console.warn( `Failed to upload payload of notification: ${notifID} ` + `due to error: ${blobUploadError}`, ); } let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders; if ( blobHash && encryptionKey && blobHolders && blobHolders.length === devicesWithExcessiveSizeNoHolders.length ) { notification.data = { ...notification.data, blobHash, encryptionKey, }; devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({ ...devicesWithExcessiveSize[idx], blobHolder: holder, })); } const notifsWithoutMessageInfos = await prepareEncryptedAndroidVisualNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesWithExcessiveSize, notification, ); const targetedNotifsWithMessageInfos = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded) .map(({ notification: notif, deliveryID, encryptionOrder }) => ({ priority, notification: notif, deliveryID, encryptionOrder, })); const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map( ({ notification: notif, deliveryID, encryptionOrder }) => ({ priority, notification: notif, deliveryID, encryptionOrder, }), ); return [ ...targetedNotifsWithMessageInfos, ...targetedNotifsWithoutMessageInfos, ]; } type AndroidNotificationRescindInputData = { +senderDeviceDescriptor: SenderDeviceDescriptor, +threadID: string, +rescindID?: string, +badge?: string, +platformDetails: PlatformDetails, }; async function createAndroidNotificationRescind( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: AndroidNotificationRescindInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { senderDeviceDescriptor, platformDetails, threadID, rescindID, badge, } = inputData; let notification = { data: { rescind: 'true', setUnreadStatus: 'true', threadID, }, }; - invariant( - (rescindID && badge) || - hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }), - 'thick thread rescind not support for this client version', - ); - if (rescindID && badge) { notification = { ...notification, data: { ...notification.data, badge, rescindID, }, }; } const shouldBeEncrypted = hasMinCodeVersion(platformDetails, { native: 233 }); if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ notification, deliveryID, priority: 'normal', })); } const notifications = await prepareEncryptedAndroidSilentNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, ); return notifications.map(({ deliveryID, notification: notif }) => ({ deliveryID, notification: notif, priority: 'normal', })); } type SenderDescriptorWithPlatformDetails = { +senderDeviceDescriptor: SenderDeviceDescriptor, +platformDetails: PlatformDetails, }; type AndroidBadgeOnlyNotificationInputData = $ReadOnly< | { ...SenderDescriptorWithPlatformDetails, +badge: string, } | { ...SenderDescriptorWithPlatformDetails, +threadID: string }, >; async function createAndroidBadgeOnlyNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: AndroidBadgeOnlyNotificationInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { senderDeviceDescriptor, platformDetails, badge, threadID } = inputData; - invariant( - (!threadID && badge) || - hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }), - 'thick thread badge updates not support for this client version', - ); - let notificationData = { badgeOnly: '1' }; if (badge) { notificationData = { ...notificationData, badge, }; } else { invariant( threadID, 'Either badge or threadID must be present in badge only notif', ); notificationData = { ...notificationData, threadID, }; } const notification: AndroidBadgeOnlyNotification = { data: notificationData }; const shouldBeEncrypted = hasMinCodeVersion(platformDetails, { native: 222 }); if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ notification, deliveryID, priority: 'normal', })); } const notifications = await prepareEncryptedAndroidSilentNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, ); return notifications.map( ({ notification: notif, deliveryID, encryptionOrder }) => ({ priority: 'normal', notification: notif, deliveryID, encryptionOrder, }), ); } export { createAndroidVisualNotification, createAndroidBadgeOnlyNotification, createAndroidNotificationRescind, }; diff --git a/lib/push/apns-notif-creators.js b/lib/push/apns-notif-creators.js index 9f93cb56c..eb48b8883 100644 --- a/lib/push/apns-notif-creators.js +++ b/lib/push/apns-notif-creators.js @@ -1,508 +1,494 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { type CommonNativeNotifInputData, commonNativeNotifInputDataValidator, } from './android-notif-creators.js'; import { prepareEncryptedAPNsVisualNotifications, prepareEncryptedAPNsSilentNotifications, } from './crypto.js'; import { getAPNsNotificationTopic } from '../shared/notif-utils.js'; -import { - hasMinCodeVersion, - FUTURE_CODE_VERSION, -} from '../shared/version-utils.js'; +import { hasMinCodeVersion } from '../shared/version-utils.js'; import type { PlatformDetails } from '../types/device-types.js'; import { messageTypes } from '../types/message-types-enum.js'; import { type NotificationTargetDevice, type EncryptedNotifUtilsAPI, type TargetedAPNsNotification, type APNsVisualNotification, type APNsNotificationHeaders, type SenderDeviceDescriptor, } from '../types/notif-types.js'; import { tShape } from '../utils/validation-utils.js'; export const apnMaxNotificationPayloadByteSize = 4096; export type APNsNotifInputData = $ReadOnly<{ ...CommonNativeNotifInputData, +uniqueID: string, }>; export const apnsNotifInputDataValidator: TInterface = tShape({ ...commonNativeNotifInputDataValidator.meta.props, uniqueID: t.String, }); async function createAPNsVisualNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: APNsNotifInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { senderDeviceDescriptor, notifTexts, newRawMessageInfos, threadID, collapseKey, badgeOnly, unreadCount, platformDetails, uniqueID, } = inputData; const canDecryptNonCollapsibleTextIOSNotifs = hasMinCodeVersion( platformDetails, { native: 222 }, ); const isNonCollapsibleTextNotification = newRawMessageInfos.every( newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, ) && !collapseKey; const canDecryptAllIOSNotifs = hasMinCodeVersion(platformDetails, { native: 267, }); const canDecryptIOSNotif = platformDetails.platform === 'ios' && (canDecryptAllIOSNotifs || (isNonCollapsibleTextNotification && canDecryptNonCollapsibleTextIOSNotifs)); const canDecryptMacOSNotifs = platformDetails.platform === 'macos' && hasMinCodeVersion(platformDetails, { web: 47, majorDesktop: 9, }); let apsDictionary = { 'thread-id': threadID, }; if (unreadCount !== undefined && unreadCount !== null) { apsDictionary = { ...apsDictionary, badge: unreadCount, }; } const { merged, ...rest } = notifTexts; // We don't include alert's body on macos because we // handle displaying the notification ourselves and // we don't want macOS to display it automatically. if (!badgeOnly && platformDetails.platform !== 'macos') { apsDictionary = { ...apsDictionary, alert: merged, sound: 'default', }; } if (hasMinCodeVersion(platformDetails, { native: 198 })) { apsDictionary = { ...apsDictionary, 'mutable-content': 1, }; } let notificationPayload = { ...rest, id: uniqueID, threadID, }; let notificationHeaders: APNsNotificationHeaders = { 'apns-topic': getAPNsNotificationTopic(platformDetails), 'apns-id': uniqueID, 'apns-push-type': 'alert', }; if (collapseKey && (canDecryptAllIOSNotifs || canDecryptMacOSNotifs)) { notificationPayload = { ...notificationPayload, collapseID: collapseKey, }; } else if (collapseKey) { notificationHeaders = { ...notificationHeaders, 'apns-collapse-id': collapseKey, }; } const notification = { ...notificationPayload, headers: notificationHeaders, aps: apsDictionary, }; const messageInfos = JSON.stringify(newRawMessageInfos); const copyWithMessageInfos = { ...notification, messageInfos, }; const notificationSizeValidator = (notif: APNsVisualNotification) => { const { headers, ...notifSansHeaders } = notif; return ( encryptedNotifUtilsAPI.getNotifByteSize( JSON.stringify(notifSansHeaders), ) <= apnMaxNotificationPayloadByteSize ); }; const serializeAPNsNotif = (notif: APNsVisualNotification) => { const { headers, ...notifSansHeaders } = notif; return JSON.stringify(notifSansHeaders); }; const shouldBeEncrypted = canDecryptIOSNotif || canDecryptMacOSNotifs; if (!shouldBeEncrypted) { const notificationToSend = notificationSizeValidator(copyWithMessageInfos) ? copyWithMessageInfos : notification; return devices.map(({ deliveryID }) => ({ notification: notificationToSend, deliveryID, })); } // The `messageInfos` field in notification payload is // not used on MacOS so we can return early. if (platformDetails.platform === 'macos') { const macOSNotifsWithoutMessageInfos = await prepareEncryptedAPNsVisualNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, platformDetails.codeVersion, ); return macOSNotifsWithoutMessageInfos.map( ({ notification: notif, deliveryID }) => ({ notification: notif, deliveryID, }), ); } const notifsWithMessageInfos = await prepareEncryptedAPNsVisualNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, copyWithMessageInfos, platformDetails.codeVersion, notificationSizeValidator, ); const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => payloadSizeExceeded) .map(({ cryptoID, deliveryID }) => ({ cryptoID, deliveryID, })); if (devicesWithExcessiveSizeNoHolders.length === 0) { return notifsWithMessageInfos.map( ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }) => ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }), ); } const canQueryBlobService = hasMinCodeVersion(platformDetails, { native: 331, }); let blobHash, blobHolders, encryptionKey, blobUploadError; if (canQueryBlobService) { ({ blobHash, blobHolders, encryptionKey, blobUploadError } = await encryptedNotifUtilsAPI.uploadLargeNotifPayload( serializeAPNsNotif(copyWithMessageInfos), devicesWithExcessiveSizeNoHolders.length, )); } if (blobUploadError) { console.warn( `Failed to upload payload of notification: ${uniqueID} ` + `due to error: ${blobUploadError}`, ); } let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders; let notificationWithBlobMetadata = notification; if ( blobHash && encryptionKey && blobHolders && blobHolders.length === devicesWithExcessiveSize.length ) { notificationWithBlobMetadata = { ...notification, blobHash, encryptionKey, }; devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({ ...devicesWithExcessiveSize[idx], blobHolder: holder, })); } const notifsWithoutMessageInfos = await prepareEncryptedAPNsVisualNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesWithExcessiveSize, notificationWithBlobMetadata, platformDetails.codeVersion, ); const targetedNotifsWithMessageInfos = notifsWithMessageInfos .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded) .map( ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }) => ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }), ); const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map( ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }) => ({ notification: notif, deliveryID, encryptedPayloadHash, encryptionOrder, }), ); return [ ...targetedNotifsWithMessageInfos, ...targetedNotifsWithoutMessageInfos, ]; } type APNsNotificationRescindInputData = { +senderDeviceDescriptor: SenderDeviceDescriptor, +rescindID?: string, +badge?: number, +threadID: string, +platformDetails: PlatformDetails, }; async function createAPNsNotificationRescind( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: APNsNotificationRescindInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { badge, rescindID, threadID, platformDetails, senderDeviceDescriptor, } = inputData; - invariant( - (rescindID && badge !== null && badge !== undefined) || - hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }), - 'thick thread rescind not support for this client version', - ); - const apnsTopic = getAPNsNotificationTopic(platformDetails); let notification; if ( rescindID && badge !== null && badge !== undefined && hasMinCodeVersion(platformDetails, { native: 198 }) ) { notification = { headers: { 'apns-topic': apnsTopic, 'apns-push-type': 'alert', }, aps: { 'mutable-content': 1, 'badge': badge, }, threadID, notificationId: rescindID, backgroundNotifType: 'CLEAR', setUnreadStatus: true, }; } else if (rescindID && badge !== null && badge !== undefined) { notification = { headers: { 'apns-topic': apnsTopic, 'apns-push-type': 'background', 'apns-priority': 5, }, aps: { 'mutable-content': 1, 'badge': badge, }, threadID, notificationId: rescindID, backgroundNotifType: 'CLEAR', setUnreadStatus: true, }; } else { notification = { headers: { 'apns-topic': apnsTopic, 'apns-push-type': 'alert', }, aps: { 'mutable-content': 1, }, threadID, backgroundNotifType: 'CLEAR', setUnreadStatus: true, }; } const shouldBeEncrypted = hasMinCodeVersion(platformDetails, { native: 233 }); if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ notification, deliveryID, })); } const notifications = await prepareEncryptedAPNsSilentNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, platformDetails.codeVersion, ); return notifications.map(({ deliveryID, notification: notif }) => ({ deliveryID, notification: notif, })); } type SenderDescriptorWithPlatformDetails = { +senderDeviceDescriptor: SenderDeviceDescriptor, +platformDetails: PlatformDetails, }; type APNsBadgeOnlyNotificationInputData = $ReadOnly< | { ...SenderDescriptorWithPlatformDetails, +badge: string, } | { ...SenderDescriptorWithPlatformDetails, +threadID: string }, >; async function createAPNsBadgeOnlyNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: APNsBadgeOnlyNotificationInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { senderDeviceDescriptor, platformDetails, threadID, badge } = inputData; - invariant( - (!threadID && badge !== undefined && badge !== null) || - hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }), - 'thick thread badge updates not support for this client version', - ); const shouldBeEncrypted = hasMinCodeVersion(platformDetails, { native: 222, web: 47, majorDesktop: 9, }); const headers: APNsNotificationHeaders = { 'apns-topic': getAPNsNotificationTopic(platformDetails), 'apns-push-type': 'alert', }; let notification; if (shouldBeEncrypted && threadID) { notification = { headers, threadID, aps: { 'mutable-content': 1, }, }; } else if (shouldBeEncrypted && badge !== undefined && badge !== null) { notification = { headers, aps: { 'badge': badge, 'mutable-content': 1, }, }; } else { invariant( badge !== null && badge !== undefined, 'badge update must contain either badge count or threadID', ); notification = { headers, aps: { badge, }, }; } if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ deliveryID, notification, })); } const notifications = await prepareEncryptedAPNsSilentNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, platformDetails.codeVersion, ); return notifications.map(({ deliveryID, notification: notif }) => ({ deliveryID, notification: notif, })); } export { createAPNsBadgeOnlyNotification, createAPNsNotificationRescind, createAPNsVisualNotification, }; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js index 8854e1a3a..7ad680c30 100644 --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -1,1122 +1,1165 @@ // @flow import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; import { createAndroidVisualNotification, createAndroidBadgeOnlyNotification, createAndroidNotificationRescind, } from './android-notif-creators.js'; import { createAPNsVisualNotification, createAPNsBadgeOnlyNotification, createAPNsNotificationRescind, } from './apns-notif-creators.js'; import { stringToVersionKey, getDevicesByPlatform, generateNotifUserInfoPromise, userAllowsNotif, } from './utils.js'; import { createWebNotification } from './web-notif-creators.js'; import { createWNSNotification } from './wns-notif-creators.js'; import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js'; import { rawMessageInfoFromMessageData, createMessageInfo, shimUnsupportedRawMessageInfos, sortMessageInfoList, } from '../shared/message-utils.js'; import { pushTypes } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import { notifTextsForMessageInfo, getNotifCollapseKey, } from '../shared/notif-utils.js'; import { isMemberActive, threadInfoFromRawThreadInfo, } from '../shared/thread-utils.js'; +import { + NEXT_CODE_VERSION, + hasMinCodeVersion, +} from '../shared/version-utils.js'; import type { AuxUserInfos } from '../types/aux-user-types.js'; import type { PlatformDetails, Platform } from '../types/device-types.js'; import { identityDeviceTypeToPlatform, type IdentityPlatformDetails, } from '../types/identity-service-types.js'; import { type MessageData, type RawMessageInfo, messageDataLocalID, } from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { ResolvedNotifTexts, NotificationTargetDevice, TargetedNotificationWithPlatform, SenderDeviceDescriptor, EncryptedNotifUtilsAPI, } from '../types/notif-types.js'; import type { ThreadSubscription } from '../types/subscription-types.js'; import type { ThickRawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; import { type GetENSNames } from '../utils/ens-helpers.js'; import { type GetFCNames } from '../utils/farcaster-helpers.js'; import { promiseAll } from '../utils/promises.js'; export type Device = { +platformDetails: PlatformDetails, +deliveryID: string, +cryptoID: string, }; export type ThreadSubscriptionWithRole = $ReadOnly<{ ...ThreadSubscription, +role: ?string, }>; export type PushUserInfo = { +devices: $ReadOnlyArray, +messageInfos: RawMessageInfo[], +messageDatas: MessageData[], +subscriptions?: { +[threadID: string]: ThreadSubscriptionWithRole, }, }; export type PushInfo = { +[userID: string]: PushUserInfo }; export type CollapsableNotifInfo = { collapseKey: ?string, existingMessageInfos: RawMessageInfo[], newMessageInfos: RawMessageInfo[], }; export type FetchCollapsableNotifsResult = { +[userID: string]: $ReadOnlyArray, }; function identityPlatformDetailsToPlatformDetails( identityPlatformDetails: IdentityPlatformDetails, ): PlatformDetails { const { deviceType, ...rest } = identityPlatformDetails; return { ...rest, platform: identityDeviceTypeToPlatform[deviceType], }; } async function getPushUserInfo( messageInfos: { +[id: string]: RawMessageInfo }, thickRawThreadInfos: ThickRawThreadInfos, auxUserInfos: AuxUserInfos, messageDatas: ?$ReadOnlyArray, ): Promise<{ +pushInfos: ?PushInfo, +rescindInfos: ?PushInfo, }> { if (!messageDatas || messageDatas.length === 0) { return { pushInfos: null, rescindInfos: null }; } const threadsToMessageIndices: Map = new Map(); const newMessageInfos: RawMessageInfo[] = []; let nextNewMessageIndex = 0; for (let i = 0; i < messageDatas.length; i++) { const messageData = messageDatas[i]; const threadID = messageData.threadID; let messageIndices = threadsToMessageIndices.get(threadID); if (!messageIndices) { messageIndices = []; threadsToMessageIndices.set(threadID, messageIndices); } const newMessageIndex = nextNewMessageIndex++; messageIndices.push(newMessageIndex); const messageID = messageDataLocalID(messageData) ?? uuidv4(); const rawMessageInfo = rawMessageInfoFromMessageData( messageData, messageID, ); newMessageInfos.push(rawMessageInfo); } const pushUserThreadInfos: { [userID: string]: { devices: $ReadOnlyArray, threadsWithSubscriptions: { [threadID: string]: ThreadSubscriptionWithRole, }, }, } = {}; for (const threadID of threadsToMessageIndices.keys()) { const threadInfo = thickRawThreadInfos[threadID]; for (const memberInfo of threadInfo.members) { if ( !isMemberActive(memberInfo) || !hasPermission(memberInfo.permissions, 'visible') ) { continue; } if (pushUserThreadInfos[memberInfo.id]) { pushUserThreadInfos[memberInfo.id].threadsWithSubscriptions[threadID] = { ...memberInfo.subscription, role: memberInfo.role }; continue; } const devicesPlatformDetails = auxUserInfos[memberInfo.id].devicesPlatformDetails; if (!devicesPlatformDetails) { continue; } const devices = Object.entries(devicesPlatformDetails).map( ([deviceID, identityPlatformDetails]) => ({ platformDetails: identityPlatformDetailsToPlatformDetails( identityPlatformDetails, ), deliveryID: deviceID, cryptoID: deviceID, }), ); pushUserThreadInfos[memberInfo.id] = { devices, threadsWithSubscriptions: { [threadID]: { ...memberInfo.subscription, role: memberInfo.role }, }, }; } } const userPushInfoPromises: { [string]: Promise } = {}; const userRescindInfoPromises: { [string]: Promise } = {}; for (const userID in pushUserThreadInfos) { const pushUserThreadInfo = pushUserThreadInfos[userID]; userPushInfoPromises[userID] = (async () => { const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise({ pushType: pushTypes.NOTIF, devices: pushUserThreadInfo.devices, newMessageInfos, messageDatas, threadsToMessageIndices, threadIDs: Object.keys(pushUserThreadInfo.threadsWithSubscriptions), userNotMemberOfSubthreads: new Set(), fetchMessageInfoByID: (messageID: string) => (async () => messageInfos[messageID])(), userID, }); if (!pushInfosWithoutSubscriptions) { return null; } return { ...pushInfosWithoutSubscriptions, subscriptions: pushUserThreadInfo.threadsWithSubscriptions, }; })(); userRescindInfoPromises[userID] = (async () => { const pushInfosWithoutSubscriptions = await generateNotifUserInfoPromise({ pushType: pushTypes.RESCIND, devices: pushUserThreadInfo.devices, newMessageInfos, messageDatas, threadsToMessageIndices, threadIDs: Object.keys(pushUserThreadInfo.threadsWithSubscriptions), userNotMemberOfSubthreads: new Set(), fetchMessageInfoByID: (messageID: string) => (async () => messageInfos[messageID])(), userID, }); if (!pushInfosWithoutSubscriptions) { return null; } return { ...pushInfosWithoutSubscriptions, subscriptions: pushUserThreadInfo.threadsWithSubscriptions, }; })(); } const [pushInfo, rescindInfo] = await Promise.all([ promiseAll(userPushInfoPromises), promiseAll(userRescindInfoPromises), ]); return { pushInfos: _pickBy(Boolean)(pushInfo), rescindInfos: _pickBy(Boolean)(rescindInfo), }; } type SenderInfo = { +senderUserID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, }; type OwnDevicesPushInfo = { +devices: $ReadOnlyArray, }; function getOwnDevicesPushInfo( senderInfo: SenderInfo, auxUserInfos: AuxUserInfos, ): ?OwnDevicesPushInfo { const { senderUserID, senderDeviceDescriptor: { senderDeviceID }, } = senderInfo; if (!senderDeviceID) { return null; } const senderDevicesWithPlatformDetails = auxUserInfos[senderUserID].devicesPlatformDetails; if (!senderDevicesWithPlatformDetails) { return null; } const devices = Object.entries(senderDevicesWithPlatformDetails) .filter(([deviceID]) => deviceID !== senderDeviceID) .map(([deviceID, identityPlatformDetails]) => ({ platformDetails: identityPlatformDetailsToPlatformDetails( identityPlatformDetails, ), deliveryID: deviceID, cryptoID: deviceID, })); return { devices }; } function pushInfoToCollapsableNotifInfo(pushInfo: PushInfo): { +usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, +usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, } { const usersToCollapseKeysToInfo: { [string]: { [string]: CollapsableNotifInfo }, } = {}; const usersToCollapsableNotifInfo: { [string]: Array } = {}; for (const userID in pushInfo) { usersToCollapseKeysToInfo[userID] = {}; usersToCollapsableNotifInfo[userID] = []; for (let i = 0; i < pushInfo[userID].messageInfos.length; i++) { const rawMessageInfo = pushInfo[userID].messageInfos[i]; const messageData = pushInfo[userID].messageDatas[i]; const collapseKey = getNotifCollapseKey(rawMessageInfo, messageData); if (!collapseKey) { const collapsableNotifInfo: CollapsableNotifInfo = { collapseKey, existingMessageInfos: [], newMessageInfos: [rawMessageInfo], }; usersToCollapsableNotifInfo[userID].push(collapsableNotifInfo); continue; } if (!usersToCollapseKeysToInfo[userID][collapseKey]) { usersToCollapseKeysToInfo[userID][collapseKey] = ({ collapseKey, existingMessageInfos: [], newMessageInfos: [], }: CollapsableNotifInfo); } usersToCollapseKeysToInfo[userID][collapseKey].newMessageInfos.push( rawMessageInfo, ); } } return { usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, }; } function mergeUserToCollapsableInfo( usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, ): { +[string]: $ReadOnlyArray } { const mergedUsersToCollapsableInfo: { [string]: Array, } = {}; for (const userID in usersToCollapseKeysToInfo) { const collapseKeysToInfo = usersToCollapseKeysToInfo[userID]; mergedUsersToCollapsableInfo[userID] = [ ...usersToCollapsableNotifInfo[userID], ]; for (const collapseKey in collapseKeysToInfo) { const info = collapseKeysToInfo[collapseKey]; mergedUsersToCollapsableInfo[userID].push({ collapseKey: info.collapseKey, existingMessageInfos: sortMessageInfoList(info.existingMessageInfos), newMessageInfos: sortMessageInfoList(info.newMessageInfos), }); } } return mergedUsersToCollapsableInfo; } async function buildNotifText( rawMessageInfos: $ReadOnlyArray, userID: string, threadInfos: { +[id: string]: ThreadInfo }, subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, +badgeOnly: boolean, }> { if (!subscriptions) { return null; } const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); const newMessageInfos = []; const newRawMessageInfos = []; for (const rawMessageInfo of rawMessageInfos) { const newMessageInfo = hydrateMessageInfo(rawMessageInfo); if (newMessageInfo) { newMessageInfos.push(newMessageInfo); newRawMessageInfos.push(rawMessageInfo); } } if (newMessageInfos.length === 0) { return null; } const [{ threadID }] = newMessageInfos; const threadInfo = threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; const subscription = subscriptions[threadID]; if (!subscription) { return null; } const username = userInfos[userID] && userInfos[userID].username; const { notifAllowed, badgeOnly } = await userAllowsNotif({ subscription, userID, newMessageInfos, userInfos, username, getENSNames, }); if (!notifAllowed) { return null; } const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( newMessageInfos, threadInfo, parentThreadInfo, notifTargetUserInfo, getENSNames, getFCNames, ); if (!notifTexts) { return null; } return { notifTexts, newRawMessageInfos, badgeOnly }; } type BuildNotifsForPlatformInput< PlatformType: Platform, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput: { +platformDetails: PlatformDetails, ... }, > = { +platform: PlatformType, +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +notifCreatorCallback: ( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, input: NotifCreatorInput, devices: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, +notifCreatorInputBase: NotifCreatorinputBase, +transformInputBase: ( inputBase: NotifCreatorinputBase, platformDetails: PlatformDetails, ) => NotifCreatorInput, +versionToDevices: $ReadOnlyMap< string, $ReadOnlyArray, >, }; async function buildNotifsForPlatform< PlatformType: Platform, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput: { +platformDetails: PlatformDetails, ... }, >( input: BuildNotifsForPlatformInput< PlatformType, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput, >, ): Promise< $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, > { const { encryptedNotifUtilsAPI, versionToDevices, notifCreatorCallback, notifCreatorInputBase, platform, transformInputBase, } = input; const promises: Array< Promise< $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, >, > = []; for (const [versionKey, devices] of versionToDevices) { const { codeVersion, stateVersion, majorDesktopVersion } = stringToVersionKey(versionKey); const platformDetails = { platform, codeVersion, stateVersion, majorDesktopVersion, }; const inputData = transformInputBase( notifCreatorInputBase, platformDetails, ); promises.push( (async () => { return ( await notifCreatorCallback(encryptedNotifUtilsAPI, inputData, devices) ).map(targetedNotification => ({ platform, targetedNotification, })); })(), ); } return (await Promise.all(promises)).flat(); } type BuildNotifsForUserDevicesInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +rawMessageInfos: $ReadOnlyArray, +userID: string, +threadInfos: { +[id: string]: ThreadInfo }, +subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, +devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, }; async function buildNotifsForUserDevices( inputData: BuildNotifsForUserDevicesInputData, ): Promise> { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, rawMessageInfos, userID, threadInfos, subscriptions, userInfos, getENSNames, getFCNames, devicesByPlatform, } = inputData; const notifTextWithNewRawMessageInfos = await buildNotifText( rawMessageInfos, userID, threadInfos, subscriptions, userInfos, getENSNames, getFCNames, ); if (!notifTextWithNewRawMessageInfos) { return null; } const { notifTexts, newRawMessageInfos, badgeOnly } = notifTextWithNewRawMessageInfos; const [{ threadID }] = newRawMessageInfos; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, notifID: uuidv4(), }, versionToDevices: androidVersionToDevices, }), ); } const macosVersionToDevices = devicesByPlatform.get('macos'); if (macosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'macos', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: macosVersionToDevices, }), ); } const windowsVersionToDevices = devicesByPlatform.get('windows'); if (windowsVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'windows', encryptedNotifUtilsAPI, notifCreatorCallback: createWNSNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: windowsVersionToDevices, }), ); } const webVersionToDevices = devicesByPlatform.get('web'); if (webVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'web', encryptedNotifUtilsAPI, notifCreatorCallback: createWebNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, id: uuidv4(), }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: webVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } async function buildRescindsForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, rescindData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = rescindData; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsNotificationRescind, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidNotificationRescind, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: androidVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } async function buildBadgeUpdatesForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, badgeUpdateData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = badgeUpdateData; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: androidVersionToDevices, }), ); } const macosVersionToDevices = devicesByPlatform.get('macos'); if (macosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'macos', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: macosVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } export type PerUserTargetedNotifications = { +[userID: string]: $ReadOnlyArray, }; type BuildNotifsFromPushInfoInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +pushInfo: PushInfo, +thickRawThreadInfos: ThickRawThreadInfos, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function buildNotifsFromPushInfo( inputData: BuildNotifsFromPushInfoInputData, ): Promise { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, pushInfo, thickRawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const threadIDs = new Set(); for (const userID in pushInfo) { for (const rawMessageInfo of pushInfo[userID].messageInfos) { const threadID = rawMessageInfo.threadID; threadIDs.add(threadID); const messageSpec = messageSpecs[rawMessageInfo.type]; if (messageSpec.threadIDs) { for (const id of messageSpec.threadIDs(rawMessageInfo)) { threadIDs.add(id); } } } } const perUserBuildNotifsResultPromises: { [userID: string]: Promise<$ReadOnlyArray>, } = {}; const { usersToCollapsableNotifInfo, usersToCollapseKeysToInfo } = pushInfoToCollapsableNotifInfo(pushInfo); const mergedUsersToCollapsableInfo = mergeUserToCollapsableInfo( usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, ); for (const userID in mergedUsersToCollapsableInfo) { const threadInfos = Object.fromEntries( [...threadIDs].map(threadID => [ threadID, threadInfoFromRawThreadInfo( thickRawThreadInfos[threadID], userID, userInfos, ), ]), ); const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices); const singleNotificationPromises = []; for (const notifInfo of mergedUsersToCollapsableInfo[userID]) { singleNotificationPromises.push( // We always pass one element array here // because coalescing is not supported for // notifications generated on the client buildNotifsForUserDevices({ encryptedNotifUtilsAPI, senderDeviceDescriptor, rawMessageInfos: notifInfo.newMessageInfos, userID, threadInfos, subscriptions: pushInfo[userID].subscriptions, userInfos, getENSNames, getFCNames, devicesByPlatform, }), ); } perUserBuildNotifsResultPromises[userID] = (async () => { const singleNotificationResults = await Promise.all( singleNotificationPromises, ); return singleNotificationResults.filter(Boolean).flat(); })(); } return promiseAll(perUserBuildNotifsResultPromises); } async function createOlmSessionWithDevices( deviceIDsToUserIDs: { +[string]: string, }, olmSessionCreator: (userID: string, deviceID: string) => Promise, ): Promise { const { initializeCryptoAccount, isNotificationsSessionInitializedWithDevices, } = getConfig().olmAPI; await initializeCryptoAccount(); const deviceIDsToSessionPresence = await isNotificationsSessionInitializedWithDevices( Object.keys(deviceIDsToUserIDs), ); const olmSessionCreationPromises = []; for (const deviceID in deviceIDsToSessionPresence) { if (deviceIDsToSessionPresence[deviceID]) { continue; } olmSessionCreationPromises.push( olmSessionCreator(deviceIDsToUserIDs[deviceID], deviceID), ); } try { await Promise.allSettled(olmSessionCreationPromises); } catch (e) { // session creation may fail for some devices // but we should still pursue notification // delivery for others console.log(e); } } +function filterDevicesSupportingDMNotifs< + T: { +devices: $ReadOnlyArray, ... }, +>(devicesContainer: T): T { + return { + ...devicesContainer, + devices: devicesContainer.devices.filter(({ platformDetails }) => + hasMinCodeVersion(platformDetails, { + native: NEXT_CODE_VERSION, + web: NEXT_CODE_VERSION, + majorDesktop: NEXT_CODE_VERSION, + }), + ), + }; +} + +function filterDevicesSupportingDMNotifsForUsers< + T: { +devices: $ReadOnlyArray, ... }, +>(userToDevicesContainer: { +[userID: string]: T }): { +[userID: string]: T } { + const result: { [userID: string]: T } = {}; + for (const userID in userToDevicesContainer) { + const devicesContainer = userToDevicesContainer[userID]; + const filteredDevicesContainer = + filterDevicesSupportingDMNotifs(devicesContainer); + if (filteredDevicesContainer.devices.length === 0) { + continue; + } + result[userID] = filteredDevicesContainer; + } + + return result; +} + type PreparePushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +olmSessionCreator: (userID: string, deviceID: string) => Promise, +messageInfos: { +[id: string]: RawMessageInfo }, +thickRawThreadInfos: ThickRawThreadInfos, +auxUserInfos: AuxUserInfos, +messageDatas: ?$ReadOnlyArray, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function preparePushNotifs( inputData: PreparePushNotifsInputData, ): Promise { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, messageDatas, messageInfos, auxUserInfos, thickRawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const { pushInfos } = await getPushUserInfo( messageInfos, thickRawThreadInfos, auxUserInfos, messageDatas, ); if (!pushInfos) { return null; } + const filteredPushInfos = filterDevicesSupportingDMNotifsForUsers(pushInfos); + const deviceIDsToUserIDs: { [string]: string } = {}; - for (const userID in pushInfos) { - for (const device of pushInfos[userID].devices) { + for (const userID in filteredPushInfos) { + for (const device of filteredPushInfos[userID].devices) { deviceIDsToUserIDs[device.cryptoID] = userID; } } await createOlmSessionWithDevices(deviceIDsToUserIDs, olmSessionCreator); return await buildNotifsFromPushInfo({ encryptedNotifUtilsAPI, senderDeviceDescriptor, - pushInfo: pushInfos, + pushInfo: filteredPushInfos, thickRawThreadInfos, userInfos, getENSNames, getFCNames, }); } type PrepareOwnDevicesPushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderInfo: SenderInfo, +olmSessionCreator: (userID: string, deviceID: string) => Promise, +auxUserInfos: AuxUserInfos, +rescindData?: { threadID: string }, +badgeUpdateData?: { threadID: string }, }; async function prepareOwnDevicesPushNotifs( inputData: PrepareOwnDevicesPushNotifsInputData, ): Promise> { const { encryptedNotifUtilsAPI, senderInfo, olmSessionCreator, auxUserInfos, rescindData, badgeUpdateData, } = inputData; const ownDevicesPushInfo = getOwnDevicesPushInfo(senderInfo, auxUserInfos); if (!ownDevicesPushInfo) { return null; } + const filteredownDevicesPushInfos = + filterDevicesSupportingDMNotifs(ownDevicesPushInfo); + const { senderUserID, senderDeviceDescriptor } = senderInfo; const deviceIDsToUserIDs: { [string]: string } = {}; - for (const device of ownDevicesPushInfo.devices) { + for (const device of filteredownDevicesPushInfos.devices) { deviceIDsToUserIDs[device.cryptoID] = senderUserID; } await createOlmSessionWithDevices(deviceIDsToUserIDs, olmSessionCreator); - const devicesByPlatform = getDevicesByPlatform(ownDevicesPushInfo.devices); + const devicesByPlatform = getDevicesByPlatform( + filteredownDevicesPushInfos.devices, + ); if (rescindData) { return await buildRescindsForOwnDevices( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesByPlatform, rescindData, ); } else if (badgeUpdateData) { return await buildBadgeUpdatesForOwnDevices( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesByPlatform, badgeUpdateData, ); } else { return null; } } export { preparePushNotifs, prepareOwnDevicesPushNotifs, generateNotifUserInfoPromise, pushInfoToCollapsableNotifInfo, mergeUserToCollapsableInfo, };