diff --git a/lib/push/android-notif-creators.js b/lib/push/android-notif-creators.js index 08faf3cea..38e151f9c 100644 --- a/lib/push/android-notif-creators.js +++ b/lib/push/android-notif-creators.js @@ -1,407 +1,407 @@ // @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 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: RawMessageInfo[], + +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 = { +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 776d6f5da..9f93cb56c 100644 --- a/lib/push/apns-notif-creators.js +++ b/lib/push/apns-notif-creators.js @@ -1,502 +1,508 @@ // @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 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 = { +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 APNsBadgeOnlyNotificationInputData = { +type SenderDescriptorWithPlatformDetails = { +senderDeviceDescriptor: SenderDeviceDescriptor, - +badge?: number, - +threadID?: string, +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-hooks.react.js b/lib/push/send-hooks.react.js index 27c2f5cad..4d9ca4dc8 100644 --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -1,202 +1,235 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { preparePushNotifs, + prepareOwnDevicesPushNotifs, type PerUserTargetedNotifications, } from './send-utils.js'; import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { thickRawThreadInfosSelector } from '../selectors/thread-selectors.js'; +import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { TargetedAPNsNotification, TargetedAndroidNotification, TargetedWebNotification, TargetedWNSNotification, NotificationsCreationData, } from '../types/notif-types.js'; import { deviceToTunnelbrokerMessageTypes } from '../types/tunnelbroker/messages.js'; import type { TunnelbrokerAPNsNotif, TunnelbrokerFCMNotif, TunnelbrokerWebPushNotif, TunnelbrokerWNSNotif, } from '../types/tunnelbroker/notif-types.js'; import { getConfig } from '../utils/config.js'; -import { getContentSigningKey } from '../utils/crypto-utils.js'; import { getMessageForException } from '../utils/errors.js'; import { useSelector } from '../utils/redux-utils.js'; function apnsNotifToTunnelbrokerAPNsNotif( targetedNotification: TargetedAPNsNotification, ): TunnelbrokerAPNsNotif { const { deliveryID: deviceID, notification: { headers, ...payload }, } = targetedNotification; const newHeaders = { ...headers, 'apns-push-type': 'Alert', }; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_APNS_NOTIF, deviceID, headers: JSON.stringify(newHeaders), payload: JSON.stringify(payload), clientMessageID: uuid.v4(), }; } function androidNotifToTunnelbrokerFCMNotif( targetedNotification: TargetedAndroidNotification, ): TunnelbrokerFCMNotif { const { deliveryID: deviceID, notification: { data }, priority, } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_FCM_NOTIF, deviceID, clientMessageID: uuid.v4(), data: JSON.stringify(data), priority: priority === 'normal' ? 'NORMAL' : 'HIGH', }; } function webNotifToTunnelbrokerWebPushNotif( targetedNotification: TargetedWebNotification, ): TunnelbrokerWebPushNotif { const { deliveryID: deviceID, notification } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WEB_PUSH_NOTIF, deviceID, clientMessageID: uuid.v4(), payload: JSON.stringify(notification), }; } function wnsNotifToTunnelbrokerWNSNofif( targetedNotification: TargetedWNSNotification, ): TunnelbrokerWNSNotif { const { deliveryID: deviceID, notification } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WNS_NOTIF, deviceID, clientMessageID: uuid.v4(), payload: JSON.stringify(notification), }; } function useSendPushNotifs(): ( notifCreationData: ?NotificationsCreationData, ) => Promise { + const client = React.useContext(IdentityClientContext); + invariant(client, 'Identity context should be set'); + const { getAuthMetadata } = client; const rawMessageInfos = useSelector(state => state.messageStore.messages); const thickRawThreadInfos = useSelector(thickRawThreadInfosSelector); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); const userInfos = useSelector(state => state.userStore.userInfos); const { getENSNames } = React.useContext(ENSCacheContext); const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; const { createOlmSessionsWithPeer: olmSessionCreator } = usePeerOlmSessionsCreatorContext(); const { sendNotif } = useTunnelbroker(); const { encryptedNotifUtilsAPI } = getConfig(); return React.useCallback( async (notifCreationData: ?NotificationsCreationData) => { if (!notifCreationData) { return; } - const deviceID = await getContentSigningKey(); + const { deviceID, userID: senderUserID } = await getAuthMetadata(); + if (!deviceID || !senderUserID) { + return; + } + const senderDeviceDescriptor = { senderDeviceID: deviceID }; - const { messageDatas } = notifCreationData; + const senderInfo = { + senderUserID, + senderDeviceDescriptor, + }; + const { messageDatas, rescindData, badgeUpdateData } = notifCreationData; const pushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, messageInfos: rawMessageInfos, thickRawThreadInfos, auxUserInfos, messageDatas, userInfos, getENSNames, getFCNames, }; - const preparedPushNotifs = await preparePushNotifs( - pushNotifsPreparationInput, - ); + const ownDevicesPushNotifsPreparationInput = { + encryptedNotifUtilsAPI, + senderInfo, + olmSessionCreator, + auxUserInfos, + rescindData, + badgeUpdateData, + }; + + const [preparedPushNotifs, preparedOwnDevicesPushNotifs] = + await Promise.all([ + preparePushNotifs(pushNotifsPreparationInput), + prepareOwnDevicesPushNotifs(ownDevicesPushNotifsPreparationInput), + ]); - if (!preparedPushNotifs) { + if (!preparedPushNotifs && !prepareOwnDevicesPushNotifs) { return; } + let allPreparedPushNotifs = preparedPushNotifs; + if (preparedOwnDevicesPushNotifs && senderUserID) { + allPreparedPushNotifs = { + ...allPreparedPushNotifs, + [senderUserID]: preparedOwnDevicesPushNotifs, + }; + } + const sendPromises = []; - for (const userID in preparedPushNotifs) { - for (const notif of preparedPushNotifs[userID]) { + for (const userID in allPreparedPushNotifs) { + for (const notif of allPreparedPushNotifs[userID]) { if (notif.targetedNotification.notification.encryptionFailed) { continue; } let tunnelbrokerNotif; if (notif.platform === 'ios' || notif.platform === 'macos') { tunnelbrokerNotif = apnsNotifToTunnelbrokerAPNsNotif( notif.targetedNotification, ); } else if (notif.platform === 'android') { tunnelbrokerNotif = androidNotifToTunnelbrokerFCMNotif( notif.targetedNotification, ); } else if (notif.platform === 'web') { tunnelbrokerNotif = webNotifToTunnelbrokerWebPushNotif( notif.targetedNotification, ); } else if (notif.platform === 'windows') { tunnelbrokerNotif = wnsNotifToTunnelbrokerWNSNofif( notif.targetedNotification, ); } else { continue; } sendPromises.push( (async () => { try { await sendNotif(tunnelbrokerNotif); } catch (e) { console.log( `Failed to send notification to device: ${ tunnelbrokerNotif.deviceID }. Details: ${getMessageForException(e) ?? ''}`, ); } })(), ); } } await Promise.all(sendPromises); }, [ + getAuthMetadata, sendNotif, encryptedNotifUtilsAPI, olmSessionCreator, rawMessageInfos, thickRawThreadInfos, auxUserInfos, userInfos, getENSNames, getFCNames, ], ); } export { useSendPushNotifs }; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js index 4d661edad..61e1ea25e 100644 --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -1,837 +1,1110 @@ // @flow import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; -import { createAndroidVisualNotification } from './android-notif-creators.js'; -import { createAPNsVisualNotification } from './apns-notif-creators.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 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]: CollapsableNotifInfo[], }; 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, + messageDatas: ?$ReadOnlyArray, ): Promise<{ +pushInfos: ?PushInfo, +rescindInfos: ?PushInfo, }> { - if (messageDatas.length === 0) { + 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]: Array }, } { 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]: Array }, ): { [string]: Array } { const mergedUsersToCollapsableInfo = { ...usersToCollapsableNotifInfo }; for (const userID in usersToCollapseKeysToInfo) { const collapseKeysToInfo = usersToCollapseKeysToInfo[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) { - for (const [versionKey, devices] of iosVersionToDevices) { - const { codeVersion, stateVersion } = stringToVersionKey(versionKey); - const platformDetails = { + promises.push( + buildNotifsForPlatform({ platform: 'ios', - codeVersion, - stateVersion, - }; - const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( - newRawMessageInfos, - platformDetails, - ); - - promises.push( - (async () => { - return ( - await createAPNsVisualNotification( - encryptedNotifUtilsAPI, - { - senderDeviceDescriptor, - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID, - collapseKey: undefined, - badgeOnly, - unreadCount: undefined, - platformDetails, - uniqueID: uuidv4(), - }, - devices, - ) - ).map(targetedNotification => ({ - platform: 'ios', - targetedNotification, - })); - })(), - ); - } + 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) { - for (const [versionKey, devices] of androidVersionToDevices) { - const { codeVersion, stateVersion } = stringToVersionKey(versionKey); - const platformDetails = { + promises.push( + buildNotifsForPlatform({ platform: 'android', - codeVersion, - stateVersion, - }; - const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( - newRawMessageInfos, - platformDetails, - ); - - promises.push( - (async () => { - return ( - await createAndroidVisualNotification( - encryptedNotifUtilsAPI, - { - senderDeviceDescriptor, - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID, - collapseKey: undefined, - badgeOnly, - unreadCount: undefined, - platformDetails, - notifID: uuidv4(), - }, - devices, - ) - ).map(targetedNotification => ({ - platform: 'android', - targetedNotification, - })); - })(), - ); - } + 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) { - for (const [versionKey, devices] of macosVersionToDevices) { - const { codeVersion, stateVersion, majorDesktopVersion } = - stringToVersionKey(versionKey); - const platformDetails = { + promises.push( + buildNotifsForPlatform({ platform: 'macos', - codeVersion, - stateVersion, - majorDesktopVersion, - }; - const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( - newRawMessageInfos, - platformDetails, - ); - - promises.push( - (async () => { - return ( - await createAPNsVisualNotification( - encryptedNotifUtilsAPI, - { - senderDeviceDescriptor, - notifTexts, - newRawMessageInfos: shimmedNewRawMessageInfos, - threadID, - collapseKey: undefined, - badgeOnly, - unreadCount: undefined, - platformDetails, - uniqueID: uuidv4(), - }, - devices, - ) - ).map(targetedNotification => ({ - platform: 'macos', - targetedNotification, - })); - })(), - ); - } + 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) { - for (const [versionKey, devices] of windowsVersionToDevices) { - const { codeVersion, stateVersion, majorDesktopVersion } = - stringToVersionKey(versionKey); - const platformDetails = { + promises.push( + buildNotifsForPlatform({ platform: 'windows', - codeVersion, - stateVersion, - majorDesktopVersion, - }; - - promises.push( - (async () => { - return ( - await createWNSNotification( - encryptedNotifUtilsAPI, - { - notifTexts, - threadID, - senderDeviceDescriptor, - platformDetails, - }, - devices, - ) - ).map(targetedNotification => ({ - platform: 'windows', - targetedNotification, - })); - })(), - ); - } + encryptedNotifUtilsAPI, + notifCreatorCallback: createWNSNotification, + notifCreatorInputBase: { + senderDeviceDescriptor, + notifTexts, + threadID, + }, + transformInputBase: (inputBase, platformDetails) => ({ + ...inputBase, + platformDetails, + }), + versionToDevices: windowsVersionToDevices, + }), + ); } const webVersionToDevices = devicesByPlatform.get('web'); if (webVersionToDevices) { - for (const [versionKey, devices] of webVersionToDevices) { - const { codeVersion, stateVersion } = stringToVersionKey(versionKey); - const platformDetails = { + promises.push( + buildNotifsForPlatform({ platform: 'web', - codeVersion, - stateVersion, - }; + encryptedNotifUtilsAPI, + notifCreatorCallback: createWebNotification, + notifCreatorInputBase: { + senderDeviceDescriptor, + notifTexts, + threadID, + id: uuidv4(), + }, + transformInputBase: (inputBase, platformDetails) => ({ + ...inputBase, + platformDetails, + }), + versionToDevices: webVersionToDevices, + }), + ); + } - promises.push( - (async () => { - return ( - await createWebNotification( - encryptedNotifUtilsAPI, - { - notifTexts, - threadID, - senderDeviceDescriptor, - platformDetails, - id: uuidv4(), - }, - devices, - ) - ).map(targetedNotification => ({ - platform: 'web', - targetedNotification, - })); - })(), - ); - } + 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); + } +} + type PreparePushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +olmSessionCreator: (userID: string, deviceID: string) => Promise, +messageInfos: { +[id: string]: RawMessageInfo }, +thickRawThreadInfos: ThickRawThreadInfos, +auxUserInfos: AuxUserInfos, - +messageDatas: $ReadOnlyArray, + +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 { - initializeCryptoAccount, - isNotificationsSessionInitializedWithDevices, - } = getConfig().olmAPI; - await initializeCryptoAccount(); - const deviceIDsToUserIDs: { [string]: string } = {}; for (const userID in pushInfos) { for (const device of pushInfos[userID].devices) { deviceIDsToUserIDs[device.cryptoID] = userID; } } - 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); - } + await createOlmSessionWithDevices(deviceIDsToUserIDs, olmSessionCreator); return await buildNotifsFromPushInfo({ encryptedNotifUtilsAPI, senderDeviceDescriptor, pushInfo: pushInfos, 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 { senderUserID, senderDeviceDescriptor } = senderInfo; + const deviceIDsToUserIDs: { [string]: string } = {}; + + for (const device of ownDevicesPushInfo.devices) { + deviceIDsToUserIDs[device.cryptoID] = senderUserID; + } + + await createOlmSessionWithDevices(deviceIDsToUserIDs, olmSessionCreator); + const devicesByPlatform = getDevicesByPlatform(ownDevicesPushInfo.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, }; diff --git a/lib/shared/dm-ops/change-thread-read-status-spec.js b/lib/shared/dm-ops/change-thread-read-status-spec.js index 1f7629d5a..8f750db6b 100644 --- a/lib/shared/dm-ops/change-thread-read-status-spec.js +++ b/lib/shared/dm-ops/change-thread-read-status-spec.js @@ -1,54 +1,63 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec'; import type { DMChangeThreadReadStatusOperation } from '../../types/dm-ops'; import { updateTypes } from '../../types/update-types-enum.js'; const changeThreadReadStatusSpec: DMOperationSpec = Object.freeze({ + notificationsCreationData: async ( + dmOperation: DMChangeThreadReadStatusOperation, + ) => { + const { threadID, unread } = dmOperation; + if (unread) { + return { badgeUpdateData: { threadID } }; + } + return { rescindData: { threadID } }; + }, processDMOperation: async ( dmOperation: DMChangeThreadReadStatusOperation, ) => { const { threadID, unread, time } = dmOperation; const updateInfos = [ { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: uuid.v4(), time, threadID, unread, }, ]; return { rawMessageInfos: [], updateInfos, }; }, canBeProcessed( dmOperation: DMChangeThreadReadStatusOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { const { creatorID, threadID } = dmOperation; if (viewerID !== creatorID) { return { isProcessingPossible: false, reason: { type: 'invalid' } }; } if (!utilities.threadInfos[threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, }); export { changeThreadReadStatusSpec }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index 68c439fde..56e8e50d5 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,415 +1,420 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; import type { MessageData } from './message-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; import { tShape } from '../utils/validation-utils.js'; export type NotifTexts = { +merged: string | EntityText, +body: string | EntityText, +title: string | ThreadEntity, +prefix?: string | EntityText, }; export type ResolvedNotifTexts = { +merged: string, +body: string, +title: string, +prefix?: string, }; export const resolvedNotifTextsValidator: TInterface = tShape({ merged: t.String, body: t.String, title: t.String, prefix: t.maybe(t.String), }); -export type NotificationsCreationData = { - +messageDatas: $ReadOnlyArray, -}; +export type NotificationsCreationData = + | { + +messageDatas: $ReadOnlyArray, + } + | { + +rescindData: { threadID: string }, + } + | { +badgeUpdateData: { threadID: string } }; export type SenderDeviceDescriptor = | { +keyserverID: string } | { +senderDeviceID: string }; export const senderDeviceDescriptorValidator: TUnion = t.union([ tShape({ keyserverID: t.String }), tShape({ senderDeviceID: t.String }), ]); // Web notifs types export type PlainTextWebNotificationPayload = { +body: string, +prefix?: string, +title: string, +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; export type PlainTextWebNotification = $ReadOnly<{ +id: string, ...PlainTextWebNotificationPayload, }>; export type EncryptedWebNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +id: string, +encryptedPayload: string, +type: '0' | '1', }>; export type WebNotification = | PlainTextWebNotification | EncryptedWebNotification; // WNS notifs types export type PlainTextWNSNotification = { +body: string, +prefix?: string, +title: string, +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; export type EncryptedWNSNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string, +type: '0' | '1', }>; export type WNSNotification = | PlainTextWNSNotification | EncryptedWNSNotification; // Android notifs types export type AndroidVisualNotificationPayloadBase = $ReadOnly<{ +badge?: string, +body: string, +title: string, +prefix?: string, +threadID: string, +collapseID?: string, +badgeOnly?: '0' | '1', +encryptionFailed?: '1', }>; type AndroidSmallVisualNotificationPayload = $ReadOnly<{ ...AndroidVisualNotificationPayloadBase, +messageInfos?: string, }>; type AndroidLargeVisualNotificationPayload = $ReadOnly<{ ...AndroidVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, }>; export type AndroidVisualNotificationPayload = | AndroidSmallVisualNotificationPayload | AndroidLargeVisualNotificationPayload; type EncryptedThinThreadPayload = { +keyserverID: string, +encryptedPayload: string, +type: '0' | '1', }; type EncryptedThickThreadPayload = { +senderDeviceID: string, +encryptedPayload: string, +type: '0' | '1', }; export type AndroidVisualNotification = { +data: $ReadOnly<{ +id?: string, ... | AndroidSmallVisualNotificationPayload | AndroidLargeVisualNotificationPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }>, }; type AndroidThinThreadRescindPayload = { +badge: string, +rescind: 'true', +rescindID?: string, +setUnreadStatus: 'true', +threadID: string, +encryptionFailed?: string, }; type AndroidThickThreadRescindPayload = { +rescind: 'true', +setUnreadStatus: 'true', +threadID: string, +encryptionFailed?: string, }; export type AndroidNotificationRescind = { +data: | AndroidThinThreadRescindPayload | AndroidThickThreadRescindPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }; type AndroidKeyserverBadgeOnlyPayload = { +badge: string, +badgeOnly: '1', +encryptionFailed?: string, }; type AndroidThickThreadBadgeOnlyPayload = { +threadID: string, +badgeOnly: '1', +encryptionFailed?: string, }; export type AndroidBadgeOnlyNotification = { +data: | AndroidKeyserverBadgeOnlyPayload | AndroidThickThreadBadgeOnlyPayload | EncryptedThinThreadPayload | EncryptedThickThreadPayload, }; type AndroidNotificationWithPriority = | { +notification: AndroidVisualNotification, +priority: 'high', } | { +notification: AndroidBadgeOnlyNotification | AndroidNotificationRescind, +priority: 'normal', }; // APNs notifs types export type APNsNotificationTopic = | 'app.comm.macos' | 'app.comm' | 'org.squadcal.app'; export type APNsNotificationHeaders = { +'apns-priority'?: 1 | 5 | 10, +'apns-id'?: string, +'apns-expiration'?: number, +'apns-topic': APNsNotificationTopic, +'apns-collapse-id'?: string, +'apns-push-type': 'background' | 'alert' | 'voip', }; type EncryptedAPNsSilentNotificationsAps = { +'mutable-content': number, +'alert'?: { body: 'ENCRYPTED' }, }; export type EncryptedAPNsSilentNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +headers: APNsNotificationHeaders, +encryptedPayload: string, +type: '1' | '0', +aps: EncryptedAPNsSilentNotificationsAps, }>; type EncryptedAPNsVisualNotificationAps = $ReadOnly<{ ...EncryptedAPNsSilentNotificationsAps, +sound?: string, }>; export type EncryptedAPNsVisualNotification = $ReadOnly<{ ...EncryptedAPNsSilentNotification, +aps: EncryptedAPNsVisualNotificationAps, +id: string, }>; type APNsVisualNotificationPayloadBase = { +aps: { +'badge'?: string | number, +'alert'?: string | { +body?: string, ... }, +'thread-id': string, +'mutable-content'?: number, +'sound'?: string, }, +body: string, +title: string, +prefix?: string, +threadID: string, +collapseID?: string, +encryptionFailed?: '1', }; type APNsSmallVisualNotificationPayload = $ReadOnly<{ ...APNsVisualNotificationPayloadBase, +messageInfos?: string, }>; type APNsLargeVisualNotificationPayload = $ReadOnly<{ ...APNsVisualNotificationPayloadBase, +blobHash: string, +encryptionKey: string, }>; export type APNsVisualNotification = | $ReadOnly<{ +headers: APNsNotificationHeaders, +id: string, ... | APNsSmallVisualNotificationPayload | APNsLargeVisualNotificationPayload, }> | EncryptedAPNsVisualNotification; type APNsLegacyRescindPayload = { +backgroundNotifType: 'CLEAR', +notificationId: string, +setUnreadStatus: true, +threadID: string, +aps: { +'badge': string | number, +'content-available': number, }, }; type APNsKeyserverRescindPayload = { +backgroundNotifType: 'CLEAR', +notificationId: string, +setUnreadStatus: true, +threadID: string, +aps: { +'badge': string | number, +'mutable-content': number, }, }; type APNsThickThreadRescindPayload = { +backgroundNotifType: 'CLEAR', +setUnreadStatus: true, +threadID: string, +aps: { +'mutable-content': number, }, }; export type APNsNotificationRescind = | $ReadOnly<{ +headers: APNsNotificationHeaders, +encryptionFailed?: '1', ... | APNsLegacyRescindPayload | APNsKeyserverRescindPayload | APNsThickThreadRescindPayload, }> | EncryptedAPNsSilentNotification; type APNsLegacyBadgeOnlyNotification = { +aps: { +badge: string | number, }, }; type APNsKeyserverBadgeOnlyNotification = { +aps: { +'badge': string | number, +'mutable-content': number, }, }; type APNsThickThreadBadgeOnlyNotification = { +aps: { +'mutable-content': number, }, +threadID: string, }; export type APNsBadgeOnlyNotification = | $ReadOnly<{ +headers: APNsNotificationHeaders, +encryptionFailed?: '1', ... | APNsLegacyBadgeOnlyNotification | APNsKeyserverBadgeOnlyNotification | APNsThickThreadBadgeOnlyNotification, }> | EncryptedAPNsSilentNotification; export type APNsNotification = | APNsVisualNotification | APNsNotificationRescind | APNsBadgeOnlyNotification; export type TargetedAPNsNotification = { +notification: APNsNotification, +deliveryID: string, +encryptedPayloadHash?: string, +encryptionOrder?: number, }; export type TargetedAndroidNotification = $ReadOnly<{ ...AndroidNotificationWithPriority, +deliveryID: string, +encryptionOrder?: number, }>; export type TargetedWebNotification = { +notification: WebNotification, +deliveryID: string, +encryptionOrder?: number, }; export type TargetedWNSNotification = { +notification: WNSNotification, +deliveryID: string, +encryptionOrder?: number, }; export type NotificationTargetDevice = { +cryptoID: string, +deliveryID: string, +blobHolder?: string, }; export type TargetedNotificationWithPlatform = | { +platform: 'ios' | 'macos', +targetedNotification: TargetedAPNsNotification, } | { +platform: 'android', +targetedNotification: TargetedAndroidNotification } | { +platform: 'web', +targetedNotification: TargetedWebNotification } | { +platform: 'windows', +targetedNotification: TargetedWNSNotification }; export type EncryptedNotifUtilsAPI = { +encryptSerializedNotifPayload: ( cryptoID: string, unencryptedPayload: string, encryptedPayloadSizeValidator?: ( encryptedPayload: string, type: '1' | '0', ) => boolean, ) => Promise<{ +encryptedData: EncryptResult, +sizeLimitViolated?: boolean, +encryptionOrder?: number, }>, +uploadLargeNotifPayload: ( payload: string, numberOfHolders: number, ) => Promise< | { +blobHolders: $ReadOnlyArray, +blobHash: string, +encryptionKey: string, } | { +blobUploadError: string }, >, +getNotifByteSize: (serializedNotification: string) => number, +getEncryptedNotifHash: (serializedNotification: string) => Promise, };