diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js index 66437ec31..7db262720 100644 --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -1,48 +1,60 @@ // @flow import * as React from 'react'; import { preparePushNotifs, - type PerUserNotifBuildResult, + 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 type { MessageData } from '../types/message-types.js'; +import type { + EncryptedNotifUtilsAPI, + SenderDeviceDescriptor, +} from '../types/notif-types.js'; import { useSelector } from '../utils/redux-utils.js'; function usePreparePushNotifs(): ( + encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceDescriptor: SenderDeviceDescriptor, messageDatas: $ReadOnlyArray, -) => Promise { +) => Promise { const rawMessageInfos = useSelector(state => state.messageStore.messages); const rawThreadInfos = useSelector(state => state.threadStore.threadInfos); 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; return React.useCallback( - (messageDatas: $ReadOnlyArray) => { + ( + encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceDescriptor: SenderDeviceDescriptor, + messageDatas: $ReadOnlyArray, + ) => { return preparePushNotifs({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, messageInfos: rawMessageInfos, rawThreadInfos, auxUserInfos, messageDatas, userInfos, getENSNames, getFCNames, }); }, [ rawMessageInfos, rawThreadInfos, auxUserInfos, userInfos, getENSNames, getFCNames, ], ); } export { usePreparePushNotifs }; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js index d5afd2e20..714350d83 100644 --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -1,373 +1,642 @@ // @flow import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; -import { generateNotifUserInfoPromise } from './utils.js'; +import { createAndroidVisualNotification } from './android-notif-creators.js'; +import { createAPNsVisualNotification } from './apns-notif-creators.js'; +import { + stringToVersionKey, + getDevicesByPlatform, + generateNotifUserInfoPromise, +} 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, } from '../shared/message-utils.js'; import { pushTypes } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import { notifTextsForMessageInfo } 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 } from '../types/device-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 } from '../types/notif-types.js'; +import type { + ResolvedNotifTexts, + NotificationTargetDevice, + TargetedNotificationWithPlatform, + SenderDeviceDescriptor, + EncryptedNotifUtilsAPI, +} from '../types/notif-types.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.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 PushUserInfo = { +devices: $ReadOnlyArray, +messageInfos: RawMessageInfo[], +messageDatas: MessageData[], }; export type PushInfo = { +[userID: string]: PushUserInfo }; type PushUserThreadInfo = { +devices: $ReadOnlyArray, +threadIDs: Set, }; function identityPlatformDetailsToPlatformDetails( identityPlatformDetails: IdentityPlatformDetails, ): PlatformDetails { const { deviceType, ...rest } = identityPlatformDetails; return { ...rest, platform: identityDeviceTypeToPlatform[deviceType], }; } async function getPushUserInfo( messageInfos: { +[id: string]: RawMessageInfo }, rawThreadInfos: RawThreadInfos, auxUserInfos: AuxUserInfos, messageDatas: $ReadOnlyArray, ): Promise<{ +pushInfos: ?PushInfo, +rescindInfos: ?PushInfo, }> { if (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]: PushUserThreadInfo } = {}; for (const threadID of threadsToMessageIndices.keys()) { const threadInfo = rawThreadInfos[threadID]; for (const memberInfo of threadInfo.members) { if ( !isMemberActive(memberInfo) || !hasPermission(memberInfo.permissions, 'visible') || !memberInfo.subscription ) { continue; } if (pushUserThreadInfos[memberInfo.id]) { pushUserThreadInfos[memberInfo.id].threadIDs.add(threadID); 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, threadIDs: new Set([threadID]), }; } } const userPushInfoPromises: { [string]: Promise } = {}; const userRescindInfoPromises: { [string]: Promise } = {}; for (const userID in pushUserThreadInfos) { const pushUserThreadInfo = pushUserThreadInfos[userID]; userPushInfoPromises[userID] = generateNotifUserInfoPromise({ pushType: pushTypes.NOTIF, devices: pushUserThreadInfo.devices, newMessageInfos, messageDatas, threadsToMessageIndices, threadIDs: pushUserThreadInfo.threadIDs, userNotMemberOfSubthreads: new Set(), fetchMessageInfoByID: (messageID: string) => (async () => messageInfos[messageID])(), userID, }); userRescindInfoPromises[userID] = generateNotifUserInfoPromise({ pushType: pushTypes.RESCIND, devices: pushUserThreadInfo.devices, newMessageInfos, messageDatas, threadsToMessageIndices, threadIDs: pushUserThreadInfo.threadIDs, userNotMemberOfSubthreads: new Set(), fetchMessageInfoByID: (messageID: string) => (async () => messageInfos[messageID])(), userID, }); } const [pushInfo, rescindInfo] = await Promise.all([ promiseAll(userPushInfoPromises), promiseAll(userRescindInfoPromises), ]); return { pushInfos: _pickBy(Boolean)(pushInfo), rescindInfos: _pickBy(Boolean)(rescindInfo), }; } async function buildNotifText( - inputData: { - +rawMessageInfos: $ReadOnlyArray, - +userID: string, - +threadInfos: { +[id: string]: ThreadInfo }, - +userInfos: UserInfos, - }, + rawMessageInfos: $ReadOnlyArray, + userID: string, + threadInfos: { +[id: string]: ThreadInfo }, + userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, }> { - const { rawMessageInfos, userID, threadInfos, userInfos } = inputData; 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; // TODO: Using types from @Ashoat check ThreadSubscription and mentioning const username = userInfos[userID] && userInfos[userID].username; const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( newMessageInfos, threadInfo, parentThreadInfo, notifTargetUserInfo, getENSNames, getFCNames, ); if (!notifTexts) { return null; } return { notifTexts, newRawMessageInfos }; } -export type PerUserNotifBuildResult = { - +[userID: string]: $ReadOnlyArray<{ - +notifTexts: ResolvedNotifTexts, - +newRawMessageInfos: $ReadOnlyArray, - }>, +type BuildNotifsForUserDevicesInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, + +rawMessageInfos: $ReadOnlyArray, + +userID: string, + +threadInfos: { +[id: string]: ThreadInfo }, + +userInfos: UserInfos, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, + +devicesByPlatform: $ReadOnlyMap< + Platform, + $ReadOnlyMap>, + >, }; -async function buildNotifsTexts( - pushInfo: PushInfo, - rawThreadInfos: RawThreadInfos, - userInfos: UserInfos, - getENSNames: ?GetENSNames, - getFCNames: ?GetFCNames, -): Promise { +async function buildNotifsForUserDevices( + inputData: BuildNotifsForUserDevicesInputData, +): Promise> { + const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + rawMessageInfos, + userID, + threadInfos, + userInfos, + getENSNames, + getFCNames, + devicesByPlatform, + } = inputData; + + const notifTextWithNewRawMessageInfos = await buildNotifText( + rawMessageInfos, + userID, + threadInfos, + userInfos, + getENSNames, + getFCNames, + ); + + if (!notifTextWithNewRawMessageInfos) { + return null; + } + + const { notifTexts, newRawMessageInfos } = 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 = { + platform: 'ios', + codeVersion, + stateVersion, + }; + const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( + newRawMessageInfos, + platformDetails, + ); + + promises.push( + (async () => { + return ( + await createAPNsVisualNotification( + encryptedNotifUtilsAPI, + { + senderDeviceDescriptor, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + badgeOnly: false, + unreadCount: undefined, + platformDetails, + uniqueID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'ios', + targetedNotification, + })); + })(), + ); + } + } + + const androidVersionToDevices = devicesByPlatform.get('android'); + if (androidVersionToDevices) { + for (const [versionKey, devices] of androidVersionToDevices) { + const { codeVersion, stateVersion } = stringToVersionKey(versionKey); + const platformDetails = { + platform: 'android', + codeVersion, + stateVersion, + }; + const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( + newRawMessageInfos, + platformDetails, + ); + + promises.push( + (async () => { + return ( + await createAndroidVisualNotification( + encryptedNotifUtilsAPI, + { + senderDeviceDescriptor, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + badgeOnly: false, + unreadCount: undefined, + platformDetails, + notifID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'android', + targetedNotification, + })); + })(), + ); + } + } + + const macosVersionToDevices = devicesByPlatform.get('macos'); + if (macosVersionToDevices) { + for (const [versionKey, devices] of macosVersionToDevices) { + const { codeVersion, stateVersion, majorDesktopVersion } = + stringToVersionKey(versionKey); + const platformDetails = { + 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: false, + unreadCount: undefined, + platformDetails, + uniqueID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'macos', + targetedNotification, + })); + })(), + ); + } + } + + const windowsVersionToDevices = devicesByPlatform.get('windows'); + if (windowsVersionToDevices) { + for (const [versionKey, devices] of windowsVersionToDevices) { + const { codeVersion, stateVersion, majorDesktopVersion } = + stringToVersionKey(versionKey); + const platformDetails = { + platform: 'windows', + codeVersion, + stateVersion, + majorDesktopVersion, + }; + + promises.push( + (async () => { + return ( + await createWNSNotification( + encryptedNotifUtilsAPI, + { + notifTexts, + threadID, + senderDeviceDescriptor, + platformDetails, + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'windows', + targetedNotification, + })); + })(), + ); + } + } + + const webVersionToDevices = devicesByPlatform.get('web'); + if (webVersionToDevices) { + for (const [versionKey, devices] of webVersionToDevices) { + const { codeVersion, stateVersion } = stringToVersionKey(versionKey); + const platformDetails = { + platform: 'web', + codeVersion, + stateVersion, + }; + + 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(); +} + +export type PerUserTargetedNotifications = { + +[userID: string]: $ReadOnlyArray, +}; + +type BuildNotifsFromPushInfoInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, + +pushInfo: PushInfo, + +rawThreadInfos: RawThreadInfos, + +userInfos: UserInfos, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, +}; + +async function buildNotifsFromPushInfo( + inputData: BuildNotifsFromPushInfoInputData, +): Promise { + const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + pushInfo, + rawThreadInfos, + 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 perUserNotifTextsPromises: { - [userID: string]: Promise< - Array, - }>, - >, + const perUserBuildNotifsResultPromises: { + [userID: string]: Promise<$ReadOnlyArray>, } = {}; for (const userID in pushInfo) { const threadInfos = Object.fromEntries( [...threadIDs].map(threadID => [ threadID, threadInfoFromRawThreadInfo( rawThreadInfos[threadID], userID, userInfos, ), ]), ); - - const userNotifTextsPromises = []; + const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices); + const singleNotificationPromises = []; for (const rawMessageInfos of pushInfo[userID].messageInfos) { - userNotifTextsPromises.push( - buildNotifText( - { - // We always pass one element array here - // because coalescing is not supported for - // notifications generated on the client - rawMessageInfos: [rawMessageInfos], - threadInfos, - userID, - userInfos, - }, + singleNotificationPromises.push( + // We always pass one element array here + // because coalescing is not supported for + // notifications generated on the client + buildNotifsForUserDevices({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + rawMessageInfos: [rawMessageInfos], + userID, + threadInfos, + userInfos, getENSNames, getFCNames, - ), + devicesByPlatform, + }), ); } - perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises); + perUserBuildNotifsResultPromises[userID] = (async () => { + const singleNotificationResults = await Promise.all( + singleNotificationPromises, + ); + return singleNotificationResults.filter(Boolean).flat(); + })(); } - const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises); - const filteredPerUserNotifTexts: { ...PerUserNotifBuildResult } = {}; - - for (const userID in perUserNotifTexts) { - filteredPerUserNotifTexts[userID] = - perUserNotifTexts[userID].filter(Boolean); - } - return filteredPerUserNotifTexts; + return promiseAll(perUserBuildNotifsResultPromises); } type PreparePushNotifsInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, +messageInfos: { +[id: string]: RawMessageInfo }, +rawThreadInfos: RawThreadInfos, +auxUserInfos: AuxUserInfos, +messageDatas: $ReadOnlyArray, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function preparePushNotifs( inputData: PreparePushNotifsInputData, -): Promise { +): Promise { const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, messageDatas, messageInfos, auxUserInfos, rawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const { pushInfos } = await getPushUserInfo( messageInfos, rawThreadInfos, auxUserInfos, messageDatas, ); if (!pushInfos) { return null; } - return await buildNotifsTexts( - pushInfos, + return await buildNotifsFromPushInfo({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + pushInfo: pushInfos, rawThreadInfos, userInfos, getENSNames, getFCNames, - ); + }); } export { preparePushNotifs, generateNotifUserInfoPromise }; diff --git a/lib/push/web-notif-creators.js b/lib/push/web-notif-creators.js index cfa2d5fc9..093e2a39a 100644 --- a/lib/push/web-notif-creators.js +++ b/lib/push/web-notif-creators.js @@ -1,70 +1,70 @@ // @flow import t, { type TInterface } from 'tcomb'; import { prepareEncryptedWebNotifications } from './crypto.js'; import { hasMinCodeVersion } from '../shared/version-utils.js'; import type { PlatformDetails } from '../types/device-types.js'; import { type NotificationTargetDevice, type TargetedWebNotification, type ResolvedNotifTexts, resolvedNotifTextsValidator, type SenderDeviceDescriptor, senderDeviceDescriptorValidator, type EncryptedNotifUtilsAPI, } from '../types/notif-types.js'; import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js'; export type WebNotifInputData = { +id: string, +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, - +unreadCount: number, + +unreadCount?: number, +platformDetails: PlatformDetails, }; export const webNotifInputDataValidator: TInterface = tShape({ id: t.String, notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceDescriptor: senderDeviceDescriptorValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); async function createWebNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: WebNotifInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { id, notifTexts, threadID, unreadCount, senderDeviceDescriptor } = inputData; const { merged, ...rest } = notifTexts; const notification = { ...rest, unreadCount, id, threadID, }; const shouldBeEncrypted = hasMinCodeVersion(inputData.platformDetails, { web: 43, }); if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ deliveryID, notification })); } return prepareEncryptedWebNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, ); } export { createWebNotification }; diff --git a/lib/push/wns-notif-creators.js b/lib/push/wns-notif-creators.js index 0db5344a2..d46e26be5 100644 --- a/lib/push/wns-notif-creators.js +++ b/lib/push/wns-notif-creators.js @@ -1,77 +1,77 @@ // @flow import t, { type TInterface } from 'tcomb'; import { prepareEncryptedWNSNotifications } from './crypto.js'; import { hasMinCodeVersion } from '../shared/version-utils.js'; import type { PlatformDetails } from '../types/device-types.js'; import { type NotificationTargetDevice, type TargetedWNSNotification, type ResolvedNotifTexts, resolvedNotifTextsValidator, type SenderDeviceDescriptor, senderDeviceDescriptorValidator, type EncryptedNotifUtilsAPI, } from '../types/notif-types.js'; import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js'; export const wnsMaxNotificationPayloadByteSize = 5000; export type WNSNotifInputData = { +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, - +unreadCount: number, + +unreadCount?: number, +platformDetails: PlatformDetails, }; export const wnsNotifInputDataValidator: TInterface = tShape({ notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceDescriptor: senderDeviceDescriptorValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); async function createWNSNotification( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, inputData: WNSNotifInputData, devices: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const { notifTexts, threadID, unreadCount, senderDeviceDescriptor } = inputData; const { merged, ...rest } = notifTexts; const notification = { ...rest, unreadCount, threadID, }; if ( encryptedNotifUtilsAPI.getNotifByteSize(JSON.stringify(notification)) > wnsMaxNotificationPayloadByteSize ) { console.warn('WNS notification exceeds size limit'); } const shouldBeEncrypted = hasMinCodeVersion(inputData.platformDetails, { majorDesktop: 10, }); if (!shouldBeEncrypted) { return devices.map(({ deliveryID }) => ({ deliveryID, notification, })); } return await prepareEncryptedWNSNotifications( encryptedNotifUtilsAPI, senderDeviceDescriptor, devices, notification, ); } export { createWNSNotification }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index 01966095a..e6ad13b73 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,390 +1,400 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; +import type { Platform } from './device-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 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, + +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, + +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', +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', }; export type EncryptedAPNsSilentNotification = $ReadOnly<{ ...SenderDeviceDescriptor, +headers: APNsNotificationHeaders, +encryptedPayload: string, +type: '1' | '0', +aps: { +'mutable-content': number, +'alert'?: { body: 'ENCRYPTED' } }, }>; export type EncryptedAPNsVisualNotification = $ReadOnly<{ ...EncryptedAPNsSilentNotification, +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: Platform, + +targetedNotification: + | TargetedAPNsNotification + | TargetedWNSNotification + | TargetedWebNotification + | TargetedAndroidNotification, +}; + 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) => string, };