diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js index d69a0eced..e69e95b9f 100644 --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -1,337 +1,343 @@ // @flow import invariant from 'invariant'; import _groupBy from 'lodash/fp/groupBy.js'; import * as React from 'react'; import uuid from 'uuid'; import type { LargeNotifData } from './crypto.js'; 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 type { AuthMetadata } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { TargetedAPNsNotification, TargetedAndroidNotification, TargetedWebNotification, TargetedWNSNotification, NotificationsCreationData, EncryptedNotifUtilsAPI, } 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 { uploadBlob, assignMultipleHolders } from '../utils/blob-service.js'; import { getConfig } from '../utils/config.js'; import { getMessageForException } from '../utils/errors.js'; import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; import { createDefaultHTTPRequestHeaders } from '../utils/services-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 { createOlmSessionsWithUser: olmSessionCreator } = usePeerOlmSessionsCreatorContext(); const { sendNotif } = useTunnelbroker(); const { encryptedNotifUtilsAPI } = getConfig(); return React.useCallback( async (notifCreationData: ?NotificationsCreationData) => { if (!notifCreationData) { return; } const authMetadata = await getAuthMetadata(); const { deviceID, userID: senderUserID } = authMetadata; if (!deviceID || !senderUserID) { return; } const senderDeviceDescriptor = { senderDeviceID: deviceID }; const senderInfo = { senderUserID, senderDeviceDescriptor, }; - const { messageDatasWithMessageInfos, rescindData, badgeUpdateData } = - notifCreationData; + const { + messageDatasWithMessageInfos, + thickRawThreadInfos, + rescindData, + badgeUpdateData, + } = notifCreationData; const pushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, messageInfos: rawMessageInfos, - thickRawThreadInfos, + notifCreationData: + messageDatasWithMessageInfos && thickRawThreadInfos + ? { + thickRawThreadInfos, + messageDatasWithMessageInfos, + } + : null, auxUserInfos, - messageDatasWithMessageInfos, userInfos, getENSNames, getFCNames, }; const ownDevicesPushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderInfo, olmSessionCreator, auxUserInfos, rescindData, badgeUpdateData, }; const [preparedPushNotifs, preparedOwnDevicesPushNotifs] = await Promise.all([ preparePushNotifs(pushNotifsPreparationInput), prepareOwnDevicesPushNotifs(ownDevicesPushNotifsPreparationInput), ]); if (!preparedPushNotifs && !prepareOwnDevicesPushNotifs) { return; } let allPreparedPushNotifs: ?PerUserTargetedNotifications = preparedPushNotifs; if (preparedOwnDevicesPushNotifs && senderUserID) { allPreparedPushNotifs = { ...allPreparedPushNotifs, [senderUserID]: { targetedNotifications: preparedOwnDevicesPushNotifs, }, }; } if (preparedPushNotifs) { try { await uploadLargeNotifBlobs( preparedPushNotifs, authMetadata, encryptedNotifUtilsAPI, ); } catch (e) { console.log('Failed to upload blobs', e); } } const sendPromises = []; for (const userID in allPreparedPushNotifs) { for (const notif of allPreparedPushNotifs[userID] .targetedNotifications) { 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, ], ); } async function uploadLargeNotifBlobs( pushNotifs: PerUserTargetedNotifications, authMetadata: AuthMetadata, encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, ): Promise { const largeNotifArray = values(pushNotifs) .map(({ largeNotifDataArray }) => largeNotifDataArray) .flat(); if (largeNotifArray.length === 0) { return; } const largeNotifsByHash: { +[blobHash: string]: $ReadOnlyArray, } = _groupBy(largeNotifData => largeNotifData.blobHash)(largeNotifArray); const uploads = Object.entries(largeNotifsByHash).map( ([blobHash, [{ encryptedCopyWithMessageInfos }]]) => ({ blobHash, encryptedCopyWithMessageInfos, }), ); const assignments = Object.entries(largeNotifsByHash) .map(([blobHash, largeNotifs]) => largeNotifs .map(({ blobHolders }) => blobHolders) .flat() .map(holder => ({ blobHash, holder })), ) .flat(); const authHeaders = createDefaultHTTPRequestHeaders(authMetadata); const uploadPromises = uploads.map( ({ blobHash, encryptedCopyWithMessageInfos }) => uploadBlob( encryptedNotifUtilsAPI.normalizeUint8ArrayForBlobUpload( encryptedCopyWithMessageInfos, ), blobHash, authHeaders, ), ); const assignmentPromise = assignMultipleHolders(assignments, authHeaders); const [uploadResults, assignmentResult] = await Promise.all([ Promise.all(uploadPromises), assignmentPromise, ]); for (const uploadResult of uploadResults) { if (uploadResult.success) { continue; } const { reason, statusText } = uploadResult; console.log( `Failed to upload. Reason: ${reason}, status text: ${statusText}`, ); } if (assignmentResult.result === 'success') { return; } if (assignmentResult.result === 'error') { const { statusText } = assignmentResult; console.log(`Failed to assign all holders. Status text: ${statusText}`); return; } for (const [blobHash, holder] of assignmentResult.failedRequests) { console.log(`Assingnemt failed for holder: ${holder} and hash ${blobHash}`); } } export { useSendPushNotifs }; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js index 81b0a2945..f25d72b65 100644 --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -1,1315 +1,1327 @@ // @flow import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; import { createAndroidVisualNotification, createAndroidBadgeOnlyNotification, createAndroidNotificationRescind, } from './android-notif-creators.js'; import { createAPNsVisualNotification, createAPNsBadgeOnlyNotification, createAPNsNotificationRescind, } from './apns-notif-creators.js'; import type { LargeNotifEncryptionResult, LargeNotifData } from './crypto'; 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 { 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 { hasMinCodeVersion } from '../shared/version-utils.js'; import type { AuxUserInfos } from '../types/aux-user-types.js'; import type { PlatformDetails, Platform } from '../types/device-types.js'; import { identityDeviceTypeToPlatform, type IdentityPlatformDetails, } from '../types/identity-service-types.js'; import { type MessageData, type RawMessageInfo, } 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 { DeviceSessionCreationRequest } from '../utils/crypto-utils.js'; import { type GetENSNames } from '../utils/ens-helpers.js'; import { type GetFCNames } from '../utils/farcaster-helpers.js'; import { values } from '../utils/objects.js'; import { promiseAll } from '../utils/promises.js'; export type Device = { +platformDetails: PlatformDetails, +deliveryID: string, +cryptoID: string, }; export type ThreadSubscriptionWithRole = $ReadOnly<{ ...ThreadSubscription, +role: ?string, }>; export type PushUserInfo = { +devices: $ReadOnlyArray, +messageInfos: RawMessageInfo[], +messageDatas: MessageData[], +subscriptions?: { +[threadID: string]: ThreadSubscriptionWithRole, }, }; export type PushInfo = { +[userID: string]: PushUserInfo }; export type CollapsableNotifInfo = { collapseKey: ?string, existingMessageInfos: RawMessageInfo[], newMessageInfos: RawMessageInfo[], }; export type FetchCollapsableNotifsResult = { +[userID: string]: $ReadOnlyArray, }; function identityPlatformDetailsToPlatformDetails( identityPlatformDetails: IdentityPlatformDetails, ): PlatformDetails { const { deviceType, ...rest } = identityPlatformDetails; return { ...rest, platform: identityDeviceTypeToPlatform[deviceType], }; } async function getPushUserInfo( messageInfos: { +[id: string]: RawMessageInfo }, - thickRawThreadInfos: ThickRawThreadInfos, auxUserInfos: AuxUserInfos, - messageDataWithMessageInfos: ?$ReadOnlyArray<{ - +messageData: MessageData, - +rawMessageInfo: RawMessageInfo, - }>, + notifCreationData: ?{ + +messageDatasWithMessageInfos: $ReadOnlyArray<{ + +messageData: MessageData, + +rawMessageInfo: RawMessageInfo, + }>, + +thickRawThreadInfos: ThickRawThreadInfos, + }, ): Promise<{ - +pushInfos: ?PushInfo, + +pushInfos: ?{ + +infos: PushInfo, + +thickRawThreadInfos: ThickRawThreadInfos, + }, +rescindInfos: ?PushInfo, }> { - if (!messageDataWithMessageInfos) { + if (!notifCreationData) { return { pushInfos: null, rescindInfos: null }; } + const { messageDatasWithMessageInfos, thickRawThreadInfos } = + notifCreationData; + const threadsToMessageIndices: Map = new Map(); const newMessageInfos: RawMessageInfo[] = []; const messageDatas: MessageData[] = []; let nextNewMessageIndex = 0; - for (const messageDataWithInfo of messageDataWithMessageInfos) { + for (const messageDataWithInfo of messageDatasWithMessageInfos) { const { messageData, rawMessageInfo } = messageDataWithInfo; const threadID = messageData.threadID; let messageIndices = threadsToMessageIndices.get(threadID); if (!messageIndices) { messageIndices = []; threadsToMessageIndices.set(threadID, messageIndices); } const newMessageIndex = nextNewMessageIndex++; messageIndices.push(newMessageIndex); messageDatas.push(messageData); 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), + pushInfos: { + infos: _pickBy(Boolean)(pushInfo), + thickRawThreadInfos, + }, rescindInfos: _pickBy(Boolean)(rescindInfo), }; } type SenderInfo = { +senderUserID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, }; type OwnDevicesPushInfo = { +devices: $ReadOnlyArray, }; function getOwnDevicesPushInfo( senderInfo: SenderInfo, auxUserInfos: AuxUserInfos, ): ?OwnDevicesPushInfo { const { senderUserID, senderDeviceDescriptor: { senderDeviceID }, } = senderInfo; if (!senderDeviceID) { return null; } const senderDevicesWithPlatformDetails = auxUserInfos[senderUserID].devicesPlatformDetails; if (!senderDevicesWithPlatformDetails) { return null; } const devices = Object.entries(senderDevicesWithPlatformDetails) .filter(([deviceID]) => deviceID !== senderDeviceID) .map(([deviceID, identityPlatformDetails]) => ({ platformDetails: identityPlatformDetailsToPlatformDetails( identityPlatformDetails, ), deliveryID: deviceID, cryptoID: deviceID, })); return { devices }; } function pushInfoToCollapsableNotifInfo(pushInfo: PushInfo): { +usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, +usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, } { const usersToCollapseKeysToInfo: { [string]: { [string]: CollapsableNotifInfo }, } = {}; const usersToCollapsableNotifInfo: { [string]: Array } = {}; for (const userID in pushInfo) { usersToCollapseKeysToInfo[userID] = {}; usersToCollapsableNotifInfo[userID] = []; for (let i = 0; i < pushInfo[userID].messageInfos.length; i++) { const rawMessageInfo = pushInfo[userID].messageInfos[i]; const messageData = pushInfo[userID].messageDatas[i]; const collapseKey = getNotifCollapseKey(rawMessageInfo, messageData); if (!collapseKey) { const collapsableNotifInfo: CollapsableNotifInfo = { collapseKey, existingMessageInfos: [], newMessageInfos: [rawMessageInfo], }; usersToCollapsableNotifInfo[userID].push(collapsableNotifInfo); continue; } if (!usersToCollapseKeysToInfo[userID][collapseKey]) { usersToCollapseKeysToInfo[userID][collapseKey] = ({ collapseKey, existingMessageInfos: [], newMessageInfos: [], }: CollapsableNotifInfo); } usersToCollapseKeysToInfo[userID][collapseKey].newMessageInfos.push( rawMessageInfo, ); } } return { usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, }; } function mergeUserToCollapsableInfo( usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, ): { +[string]: $ReadOnlyArray } { const mergedUsersToCollapsableInfo: { [string]: Array, } = {}; for (const userID in usersToCollapseKeysToInfo) { const collapseKeysToInfo = usersToCollapseKeysToInfo[userID]; mergedUsersToCollapsableInfo[userID] = [ ...usersToCollapsableNotifInfo[userID], ]; for (const collapseKey in collapseKeysToInfo) { const info = collapseKeysToInfo[collapseKey]; mergedUsersToCollapsableInfo[userID].push({ collapseKey: info.collapseKey, existingMessageInfos: sortMessageInfoList(info.existingMessageInfos), newMessageInfos: sortMessageInfoList(info.newMessageInfos), }); } } return mergedUsersToCollapsableInfo; } async function buildNotifText( rawMessageInfos: $ReadOnlyArray, userID: string, threadInfos: { +[id: string]: ThreadInfo }, subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, +badgeOnly: boolean, }> { if (!subscriptions) { return null; } const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); const newMessageInfos = []; const newRawMessageInfos = []; for (const rawMessageInfo of rawMessageInfos) { const newMessageInfo = hydrateMessageInfo(rawMessageInfo); if (newMessageInfo) { newMessageInfos.push(newMessageInfo); newRawMessageInfos.push(rawMessageInfo); } } if (newMessageInfos.length === 0) { return null; } const [{ threadID }] = newMessageInfos; const threadInfo = threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; const subscription = subscriptions[threadID]; if (!subscription) { return null; } const username = userInfos[userID] && userInfos[userID].username; const { notifAllowed, badgeOnly } = await userAllowsNotif({ subscription, userID, newMessageInfos, userInfos, username, getENSNames, }); if (!notifAllowed) { return null; } const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( newMessageInfos, threadInfo, parentThreadInfo, notifTargetUserInfo, getENSNames, getFCNames, ); if (!notifTexts) { return null; } return { notifTexts, newRawMessageInfos, badgeOnly }; } type BuildNotifsForPlatformInput< PlatformType: Platform, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput: { +platformDetails: PlatformDetails, ... }, > = { +platform: PlatformType, +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +notifCreatorCallback: ( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, input: NotifCreatorInput, devices: $ReadOnlyArray, largeNotifToEncryptionResultPromises?: { [string]: Promise, }, ) => Promise<{ +targetedNotifications: $ReadOnlyArray, +largeNotifData?: LargeNotifData, }>, +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, >, largeNotifToEncryptionResultPromises?: { [string]: Promise, }, ): Promise<{ +targetedNotificationsWithPlatform: $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, +largeNotifDataArray?: $ReadOnlyArray, }> { const { encryptedNotifUtilsAPI, versionToDevices, notifCreatorCallback, notifCreatorInputBase, platform, transformInputBase, } = input; const promises: Array< Promise<{ +targetedNotificationsWithPlatform: $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, +largeNotifData?: LargeNotifData, }>, > = []; 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 () => { const { targetedNotifications, largeNotifData } = await notifCreatorCallback( encryptedNotifUtilsAPI, inputData, devices, largeNotifToEncryptionResultPromises, ); const targetedNotificationsWithPlatform = targetedNotifications.map( targetedNotification => ({ platform, targetedNotification, }), ); return { targetedNotificationsWithPlatform, largeNotifData }; })(), ); } const results = await Promise.all(promises); const targetedNotifsWithPlatform = results .map( ({ targetedNotificationsWithPlatform }) => targetedNotificationsWithPlatform, ) .flat(); const largeNotifDataArray = results .map(({ largeNotifData }) => largeNotifData) .filter(Boolean); return { largeNotifDataArray, targetedNotificationsWithPlatform: targetedNotifsWithPlatform, }; } 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, largeNotifToEncryptionResultPromises: { [string]: Promise, }, ): Promise, +largeNotifDataArray?: $ReadOnlyArray, }> { 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<{ +targetedNotificationsWithPlatform: $ReadOnlyArray, +largeNotifDataArray?: $ReadOnlyArray, }>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform( { platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: iosVersionToDevices, }, largeNotifToEncryptionResultPromises, ), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform( { platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, notifID: uuidv4(), }, versionToDevices: androidVersionToDevices, }, largeNotifToEncryptionResultPromises, ), ); } const macosVersionToDevices = devicesByPlatform.get('macos'); if (macosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'macos', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: macosVersionToDevices, }), ); } const windowsVersionToDevices = devicesByPlatform.get('windows'); if (windowsVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'windows', encryptedNotifUtilsAPI, notifCreatorCallback: createWNSNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: windowsVersionToDevices, }), ); } const webVersionToDevices = devicesByPlatform.get('web'); if (webVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'web', encryptedNotifUtilsAPI, notifCreatorCallback: createWebNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, id: uuidv4(), }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: webVersionToDevices, }), ); } const results = await Promise.all(promises); const targetedNotifsWithPlatform = results .map( ({ targetedNotificationsWithPlatform }) => targetedNotificationsWithPlatform, ) .flat(); const largeNotifDataArray = results .map(({ largeNotifDataArray: array }) => array) .filter(Boolean) .flat(); return { largeNotifDataArray, targetedNotificationsWithPlatform: targetedNotifsWithPlatform, }; } async function buildRescindsForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, rescindData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = rescindData; const promises: Array< Promise<{ +targetedNotificationsWithPlatform: $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, }), ); } const results = await Promise.all(promises); const targetedNotifsWithPlatform = results .map( ({ targetedNotificationsWithPlatform }) => targetedNotificationsWithPlatform, ) .flat(); return targetedNotifsWithPlatform; } async function buildBadgeUpdatesForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, badgeUpdateData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = badgeUpdateData; const promises: Array< Promise<{ +targetedNotificationsWithPlatform: $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, }), ); } const results = await Promise.all(promises); const targetedNotifsWithPlatform = results .map( ({ targetedNotificationsWithPlatform }) => targetedNotificationsWithPlatform, ) .flat(); return targetedNotifsWithPlatform; } export type PerUserTargetedNotifications = { +[userID: string]: { +targetedNotifications: $ReadOnlyArray, +largeNotifDataArray?: $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<{ +targetedNotifications: $ReadOnlyArray, +largeNotifDataArray?: $ReadOnlyArray, }>, } = {}; const { usersToCollapsableNotifInfo, usersToCollapseKeysToInfo } = pushInfoToCollapsableNotifInfo(pushInfo); const mergedUsersToCollapsableInfo = mergeUserToCollapsableInfo( usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, ); const largeNotifToEncryptionResultPromises: { [string]: Promise, } = {}; 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, }, largeNotifToEncryptionResultPromises, ), ); } perUserBuildNotifsResultPromises[userID] = (async () => { const singleNotificationResults = ( await Promise.all(singleNotificationPromises) ).filter(Boolean); const targetedNotifsWithPlatform = singleNotificationResults .map( ({ targetedNotificationsWithPlatform }) => targetedNotificationsWithPlatform, ) .flat(); const largeNotifDataArray = singleNotificationResults .map(({ largeNotifDataArray: array }) => array) .filter(Boolean) .flat(); return { targetedNotifications: targetedNotifsWithPlatform, largeNotifDataArray, }; })(); } return promiseAll(perUserBuildNotifsResultPromises); } async function createOlmSessionWithDevices( userDevices: { +[userID: string]: $ReadOnlyArray, }, olmSessionCreator: ( userID: string, devices: $ReadOnlyArray, ) => Promise, ): Promise { const { initializeCryptoAccount, isNotificationsSessionInitializedWithDevices, } = getConfig().olmAPI; await initializeCryptoAccount(); const deviceIDsToSessionPresence = await isNotificationsSessionInitializedWithDevices( values(userDevices).flat(), ); const olmSessionCreationPromises = []; for (const userID in userDevices) { const devices = userDevices[userID] .filter(deviceID => !deviceIDsToSessionPresence[deviceID]) .map(deviceID => ({ deviceID, })); olmSessionCreationPromises.push(olmSessionCreator(userID, devices)); } try { // The below is equvialent to // Promise.allSettled(olmSessionCreationPromises), which appears to be // undefined in Android (at least on debug builds) as of Sept 2024 await Promise.all( olmSessionCreationPromises.map(async promise => { try { const result = await promise; return ({ status: 'fulfilled', value: result, }: $SettledPromiseResult); } catch (e) { return ({ status: 'rejected', reason: e, }: $SettledPromiseResult); } }), ); } catch (e) { // session creation may fail for some devices // but we should still pursue notification // delivery for others console.log('Olm session creation failed', e); } } function filterDevicesSupportingDMNotifs< T: { +devices: $ReadOnlyArray, ... }, >(devicesContainer: T): T { return { ...devicesContainer, devices: devicesContainer.devices.filter(({ platformDetails }) => hasMinCodeVersion(platformDetails, { native: 393, web: 115, majorDesktop: 14, }), ), }; } function filterDevicesSupportingDMNotifsForUsers< T: { +devices: $ReadOnlyArray, ... }, >(userToDevicesContainer: { +[userID: string]: T }): { +[userID: string]: T } { const result: { [userID: string]: T } = {}; for (const userID in userToDevicesContainer) { const devicesContainer = userToDevicesContainer[userID]; const filteredDevicesContainer = filterDevicesSupportingDMNotifs(devicesContainer); if (filteredDevicesContainer.devices.length === 0) { continue; } result[userID] = filteredDevicesContainer; } return result; } type PreparePushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +olmSessionCreator: ( userID: string, devices: $ReadOnlyArray, ) => Promise, +messageInfos: { +[id: string]: RawMessageInfo }, - +thickRawThreadInfos: ThickRawThreadInfos, +auxUserInfos: AuxUserInfos, - +messageDatasWithMessageInfos: ?$ReadOnlyArray<{ - +messageData: MessageData, - +rawMessageInfo: RawMessageInfo, - }>, + +notifCreationData: ?{ + +messageDatasWithMessageInfos: $ReadOnlyArray<{ + +messageData: MessageData, + +rawMessageInfo: RawMessageInfo, + }>, + +thickRawThreadInfos: ThickRawThreadInfos, + }, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function preparePushNotifs( inputData: PreparePushNotifsInputData, ): Promise { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, - messageDatasWithMessageInfos, + notifCreationData, messageInfos, auxUserInfos, - thickRawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const { pushInfos } = await getPushUserInfo( messageInfos, - thickRawThreadInfos, auxUserInfos, - messageDatasWithMessageInfos, + notifCreationData, ); if (!pushInfos) { return null; } - const filteredPushInfos = filterDevicesSupportingDMNotifsForUsers(pushInfos); + const { infos, thickRawThreadInfos } = pushInfos; + const filteredPushInfos = filterDevicesSupportingDMNotifsForUsers(infos); const userDevices: { [userID: string]: $ReadOnlyArray, } = {}; for (const userID in filteredPushInfos) { userDevices[userID] = filteredPushInfos[userID].devices.map( device => device.cryptoID, ); } await createOlmSessionWithDevices(userDevices, olmSessionCreator); return await buildNotifsFromPushInfo({ encryptedNotifUtilsAPI, senderDeviceDescriptor, pushInfo: filteredPushInfos, thickRawThreadInfos, userInfos, getENSNames, getFCNames, }); } type PrepareOwnDevicesPushNotifsInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderInfo: SenderInfo, +olmSessionCreator: ( userID: string, devices: $ReadOnlyArray, ) => Promise, +auxUserInfos: AuxUserInfos, +rescindData?: { +threadID: string }, +badgeUpdateData?: { +threadID: string }, }; async function prepareOwnDevicesPushNotifs( inputData: PrepareOwnDevicesPushNotifsInputData, ): Promise> { const { encryptedNotifUtilsAPI, senderInfo, olmSessionCreator, auxUserInfos, rescindData, badgeUpdateData, } = inputData; const ownDevicesPushInfo = getOwnDevicesPushInfo(senderInfo, auxUserInfos); if (!ownDevicesPushInfo) { return null; } const filteredOwnDevicesPushInfos = filterDevicesSupportingDMNotifs(ownDevicesPushInfo); const { senderUserID, senderDeviceDescriptor } = senderInfo; const userDevices: { +[userID: string]: $ReadOnlyArray, } = { [senderUserID]: filteredOwnDevicesPushInfos.devices.map( device => device.cryptoID, ), }; await createOlmSessionWithDevices(userDevices, olmSessionCreator); const devicesByPlatform = getDevicesByPlatform( filteredOwnDevicesPushInfos.devices, ); if (rescindData) { return await buildRescindsForOwnDevices( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesByPlatform, rescindData, ); } else if (badgeUpdateData) { return await buildBadgeUpdatesForOwnDevices( encryptedNotifUtilsAPI, senderDeviceDescriptor, devicesByPlatform, badgeUpdateData, ); } else { return null; } } export { preparePushNotifs, prepareOwnDevicesPushNotifs, generateNotifUserInfoPromise, pushInfoToCollapsableNotifInfo, mergeUserToCollapsableInfo, }; diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index 86fd604f6..21bbc63ad 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,188 +1,191 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMAddMembersOperation, dmAddMembersOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThreadPermissionsInfo } from '../../types/thread-permission-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; function createAddNewMembersMessageDataWithInfoFromDMOperation( dmOperation: DMAddMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, threadID, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function createPermissionsForNewMembers( threadInfo: ThickRawThreadInfo, utilities: ProcessDMOperationUtilities, ): { +membershipPermissions: ThreadPermissionsInfo, +roleID: string, } { const defaultRoleID = values(threadInfo.roles).find(role => roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); const { parentThreadID } = threadInfo; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while adding ` + 'thread members but is missing from the store', ); } const { membershipPermissions } = createRoleAndPermissionForThickThreads( threadInfo.type, threadInfo.id, defaultRoleID, parentThreadInfo, ); return { membershipPermissions, roleID: defaultRoleID, }; } const addMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, addedUserIDs, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; const messageDataWithMessageInfos = createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const currentThreadInfo = threadInfos[threadID]; const { membershipPermissions, roleID } = createPermissionsForNewMembers( currentThreadInfo, utilities, ); const memberTimestamps = { ...currentThreadInfo.timestamps.members }; const newMembers = []; for (const userID of addedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: currentThreadInfo.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; if (userIsMember(currentThreadInfo, userID)) { continue; } newMembers.push( minimallyEncodeMemberInfo({ id: userID, role: roleID, permissions: membershipPermissions, isSender: editorID === viewerID, subscription: joinThreadSubscription, }), ); } const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMembers], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: resultThreadInfo, + }, }; return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmAddMembersOperationValidator, }); export { addMembersSpec, createAddNewMembersMessageDataWithInfoFromDMOperation, createPermissionsForNewMembers, }; diff --git a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js index 9d502127e..6b69e682f 100644 --- a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js +++ b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js @@ -1,212 +1,221 @@ // @flow import uuid from 'uuid'; import { createPermissionsForNewMembers } from './add-members-spec.js'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMAddViewerToThreadMembersOperation, dmAddViewerToThreadMembersValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { minimallyEncodeMemberInfo, minimallyEncodeThreadCurrentUserInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { userIsMember } from '../thread-utils.js'; function createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( dmOperation: DMAddViewerToThreadMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, existingThreadDetails, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID: existingThreadDetails.threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const addViewerToThreadMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { time, messageID, addedUserIDs, existingThreadDetails, editorID } = dmOperation; const { threadInfos } = utilities; const messageDataWithMessageInfos = createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = messageID ? [rawMessageInfo] : []; const threadID = existingThreadDetails.threadID; const currentThreadInfo = threadInfos[threadID]; const memberTimestamps = { ...currentThreadInfo?.timestamps?.members, }; const newMembers = []; for (const userID of addedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: existingThreadDetails.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; if (!userIsMember(currentThreadInfo, userID)) { newMembers.push(userID); } } - const notificationsCreationData = { - messageDatasWithMessageInfos: [messageDataWithMessageInfos], - }; - if (currentThreadInfo) { const { membershipPermissions, roleID } = createPermissionsForNewMembers(currentThreadInfo, utilities); const newMemberInfos = newMembers.map(userID => minimallyEncodeMemberInfo({ id: userID, role: roleID, permissions: membershipPermissions, isSender: editorID === utilities.viewerID, subscription: joinThreadSubscription, }), ); const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMemberInfos], currentUser: minimallyEncodeThreadCurrentUserInfo({ role: roleID, permissions: membershipPermissions, subscription: joinThreadSubscription, unread: true, }), timestamps: { ...currentThreadInfo.timestamps, members: { ...currentThreadInfo.timestamps.members, ...memberTimestamps, }, }, }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: resultThreadInfo, + }, + }; + return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; } const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, ...newMembers.map(id => ({ id, subscription: joinThreadSubscription, })), ], timestamps: { ...existingThreadDetails.timestamps, members: { ...existingThreadDetails.timestamps.members, ...memberTimestamps, }, }, }, utilities, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: resultThreadInfo, + }, + }; return { rawMessageInfos: [], updateInfos, blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { viewerID } = utilities; // We expect the viewer to be in the added users when the DM op // is processed. An exception is for ops generated // by InitialStateSharingHandler, which won't contain a messageID if ( dmOperation.addedUserIDs.includes(viewerID) || !dmOperation.messageID ) { return { isProcessingPossible: true }; } console.log('Invalid DM operation', dmOperation); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; }, supportsAutoRetry: true, operationValidator: dmAddViewerToThreadMembersValidator, }); export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersMessageDataWithInfoFromDMOp, }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 2613f9a61..87d897132 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,212 +1,215 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMBlobOperation, type DMChangeThreadSettingsOperation, type DMThreadSettingsChanges, dmChangeThreadSettingsOperationValidator, } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ChangeSettingsMessageData } from '../../types/messages/change-settings.js'; import type { RawThreadInfo, ThickRawThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { blobHashFromBlobServiceURI } from '../../utils/blob-service.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function getThreadIDFromChangeThreadSettingsDMOp( dmOperation: DMChangeThreadSettingsOperation, ): string { return dmOperation.type === 'change_thread_settings' ? dmOperation.threadID : dmOperation.existingThreadDetails.threadID; } function createChangeSettingsMessageDatasAndUpdate( dmOperation: DMChangeThreadSettingsOperation, ): { +fieldNameToMessageData: { +[fieldName: string]: { +messageData: ChangeSettingsMessageData, +rawMessageInfo: RawMessageInfo, }, }, +threadInfoUpdate: DMThreadSettingsChanges, } { const { changes, editorID, time, messageIDsPrefix } = dmOperation; const { name, description, color, avatar } = changes; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfoUpdate: { ...DMThreadSettingsChanges } = {}; if (name !== undefined && name !== null) { threadInfoUpdate.name = name; } if (description !== undefined && description !== null) { threadInfoUpdate.description = description; } if (color) { threadInfoUpdate.color = color; } if (avatar || avatar === null) { threadInfoUpdate.avatar = avatar; } const fieldNameToMessageData: { [fieldName: string]: { +messageData: ChangeSettingsMessageData, +rawMessageInfo: RawMessageInfo, }, } = {}; const { avatar: avatarObject, ...rest } = threadInfoUpdate; let normalizedThreadInfoUpdate; if (avatarObject) { normalizedThreadInfoUpdate = { ...rest, avatar: JSON.stringify(avatarObject), }; } else if (avatarObject === null) { // clear thread avatar normalizedThreadInfoUpdate = { ...rest, avatar: '' }; } else { normalizedThreadInfoUpdate = { ...rest }; } for (const fieldName in normalizedThreadInfoUpdate) { const value = normalizedThreadInfoUpdate[fieldName]; const messageData: ChangeSettingsMessageData = { type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: value, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, `${messageIDsPrefix}/${fieldName}`, ); fieldNameToMessageData[fieldName] = { messageData, rawMessageInfo }; } return { fieldNameToMessageData, threadInfoUpdate }; } function getBlobOpsFromOperation( dmOperation: DMChangeThreadSettingsOperation, threadInfo: ?RawThreadInfo, ): Array { const ops: Array = []; const prevAvatar = threadInfo?.avatar; if (prevAvatar && prevAvatar.type === 'encrypted_image') { ops.push({ type: 'remove_holder', blobHash: blobHashFromBlobServiceURI(prevAvatar.blobURI), dmOpType: 'inbound_and_outbound', }); } const { avatar } = dmOperation.changes; if (avatar && avatar?.type === 'encrypted_image') { ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(avatar.blobURI), dmOpType: 'inbound_only', }); } return ops; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { const { time } = dmOperation; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfo = utilities.threadInfos[threadID]; const updateInfos: Array = []; const { fieldNameToMessageData, threadInfoUpdate } = createChangeSettingsMessageDatasAndUpdate(dmOperation); const blobOps = getBlobOpsFromOperation(dmOperation, threadInfo); const messageDataWithMessageInfoPairs = values(fieldNameToMessageData); const rawMessageInfos = messageDataWithMessageInfoPairs.map( ({ rawMessageInfo }) => rawMessageInfo, ); let threadInfoToUpdate: ThickRawThreadInfo = threadInfo; for (const fieldName in threadInfoUpdate) { const timestamp = threadInfoToUpdate.timestamps[fieldName]; if (timestamp < time) { threadInfoToUpdate = { ...threadInfoToUpdate, [fieldName]: threadInfoUpdate[fieldName], timestamps: { ...threadInfoToUpdate.timestamps, [fieldName]: time, }, }; } } if (messageDataWithMessageInfoPairs.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } const notificationsCreationData = { messageDatasWithMessageInfos: values(fieldNameToMessageData), + thickRawThreadInfos: { + [threadID]: threadInfoToUpdate, + }, }; return { rawMessageInfos, updateInfos, blobOps, notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmChangeThreadSettingsOperationValidator, }); export { changeThreadSettingsSpec, createChangeSettingsMessageDatasAndUpdate }; diff --git a/lib/shared/dm-ops/create-entry-spec.js b/lib/shared/dm-ops/create-entry-spec.js index 240a370a7..d2a1a86fc 100644 --- a/lib/shared/dm-ops/create-entry-spec.js +++ b/lib/shared/dm-ops/create-entry-spec.js @@ -1,111 +1,117 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMCreateEntryOperation, dmCreateEntryOperationValidator, } from '../../types/dm-ops.js'; import type { ThickRawEntryInfo } from '../../types/entry-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMCreateEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, text, messageID } = dmOperation; const messageData = { type: messageTypes.CREATE_ENTRY, threadID, creatorID, time, entryID, date: entryDate, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const createEntrySpec: DMOperationSpec = Object.freeze({ - processDMOperation: async (dmOperation: DMCreateEntryOperation) => { + processDMOperation: async ( + dmOperation: DMCreateEntryOperation, + utilities: ProcessDMOperationUtilities, + ) => { const { threadID, creatorID, time, entryID, entryDate, text } = dmOperation; const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const date = dateFromString(entryDate); const rawEntryInfo: ThickRawEntryInfo = { id: entryID, threadID, text, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime: time, creatorID, thick: true, deleted: false, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfo, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: utilities.threadInfos[threadID], + }, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.entryInfos[dmOperation.entryID]) { console.log( 'Discarded a CREATE_ENTRY operation because entry with ' + `the same ID ${dmOperation.entryID} already exists in the store`, ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmCreateEntryOperationValidator, }); export { createEntrySpec }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index abb3df18c..d56bceaa0 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,219 +1,222 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMCreateSidebarOperation, dmCreateSidebarOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { type RawMessageInfo, messageTruncationStatus, } from '../../types/message-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { generatePendingThreadColor } from '../color-utils.js'; import { isInvalidSidebarSource, rawMessageInfoFromMessageData, } from '../message-utils.js'; import { createThreadTimestamps } from '../thread-utils.js'; async function createMessageDatasWithInfosFromDMOperation( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, threadColor?: string, ) { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, newSidebarSourceMessageID, newCreateSidebarMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = threadColor ?? generatePendingThreadColor(allMemberIDs); const sourceMessage = await utilities.fetchMessage(sourceMessageID); if (!sourceMessage) { throw new Error( `could not find sourceMessage ${sourceMessageID}... probably ` + 'joined thick thread ${parentThreadID} after its creation', ); } if (isInvalidSidebarSource(sourceMessage)) { throw new Error( `sourceMessage ${sourceMessageID} is an invalid sidebar source`, ); } const sidebarSourceMessageData = { type: messageTypes.SIDEBAR_SOURCE, threadID, creatorID, time, sourceMessage: sourceMessage, }; const createSidebarMessageData = { type: messageTypes.CREATE_SIDEBAR, threadID, creatorID, time: time + 1, sourceMessageAuthorID: sourceMessage.creatorID, initialThreadState: { parentThreadID, color, memberIDs: allMemberIDs, }, }; const sidebarSourceMessageInfo = rawMessageInfoFromMessageData( sidebarSourceMessageData, newSidebarSourceMessageID, ); const createSidebarMessageInfo = rawMessageInfoFromMessageData( createSidebarMessageData, newCreateSidebarMessageID, ); return { sidebarSourceMessageData, createSidebarMessageData, sidebarSourceMessageInfo, createSidebarMessageInfo, }; } const createSidebarSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, roleID, } = dmOperation; const { viewerID } = utilities; const allMemberIDs = [creatorID, ...memberIDs]; const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ id, subscription: joinThreadSubscription, })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, sourceMessageID, containingThreadID: parentThreadID, timestamps: createThreadTimestamps(time, allMemberIDs), }, utilities, ); const { sidebarSourceMessageData, createSidebarMessageData, createSidebarMessageInfo, sidebarSourceMessageInfo, } = await createMessageDatasWithInfosFromDMOperation( dmOperation, utilities, rawThreadInfo.color, ); const rawMessageInfos: Array = [ sidebarSourceMessageInfo, createSidebarMessageInfo, ]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; const notificationsCreationData = { messageDatasWithMessageInfos: [ { messageData: sidebarSourceMessageData, rawMessageInfo: sidebarSourceMessageInfo, }, { messageData: createSidebarMessageData, rawMessageInfo: createSidebarMessageInfo, }, ], + thickRawThreadInfos: { + [threadID]: rawThreadInfo, + }, }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { console.log( 'Discarded a CREATE_SIDEBAR operation because thread ' + `with the same ID ${dmOperation.threadID} already exists ` + 'in the store', ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } const sourceMessage = await utilities.fetchMessage( dmOperation.sourceMessageID, ); if (!sourceMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.sourceMessageID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmCreateSidebarOperationValidator, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index 5c86259d1..42661a79e 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,315 +1,318 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { specialRoles } from '../../permissions/special-roles.js'; import { getAllThreadPermissions, makePermissionsBlob, getThickThreadRolePermissionsBlob, makePermissionsForChildrenBlob, } from '../../permissions/thread-permissions.js'; import { type CreateThickRawThreadInfoInput, type DMCreateThreadOperation, dmCreateThreadOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import { type ThickRawThreadInfo, type RoleInfo, minimallyEncodeMemberInfo, minimallyEncodeRoleInfo, minimallyEncodeThreadCurrentUserInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThreadPermissionsInfo } from '../../types/thread-permission-types.js'; import type { ThickThreadType } from '../../types/thread-types-enum.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { generatePendingThreadColor } from '../color-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { createThreadTimestamps } from '../thread-utils.js'; function createPermissionsInfo( threadID: string, threadType: ThickThreadType, isMember: boolean, parentThreadInfo: ?ThickRawThreadInfo, ): ThreadPermissionsInfo { let rolePermissions = null; if (isMember) { rolePermissions = getThickThreadRolePermissionsBlob(threadType); } let permissionsFromParent = null; if (parentThreadInfo) { const parentThreadRolePermissions = getThickThreadRolePermissionsBlob( parentThreadInfo.type, ); const parentPermissionsBlob = makePermissionsBlob( parentThreadRolePermissions, null, parentThreadInfo.id, parentThreadInfo.type, ); permissionsFromParent = makePermissionsForChildrenBlob( parentPermissionsBlob, ); } return getAllThreadPermissions( makePermissionsBlob( rolePermissions, permissionsFromParent, threadID, threadType, ), threadID, ); } function createRoleAndPermissionForThickThreads( threadType: ThickThreadType, threadID: string, roleID: string, parentThreadInfo: ?ThickRawThreadInfo, ): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { const rolePermissions = getThickThreadRolePermissionsBlob(threadType); const membershipPermissions = createPermissionsInfo( threadID, threadType, true, parentThreadInfo, ); const role: RoleInfo = { ...minimallyEncodeRoleInfo({ id: roleID, name: 'Members', permissions: rolePermissions, isDefault: true, }), specialRole: specialRoles.DEFAULT_ROLE, }; return { membershipPermissions, role, }; } type MutableThickRawThreadInfo = { ...ThickRawThreadInfo }; function createThickRawThreadInfo( input: CreateThickRawThreadInfoInput, utilities: ProcessDMOperationUtilities, ): MutableThickRawThreadInfo { const { threadID, threadType, creationTime, parentThreadID, allMemberIDsWithSubscriptions, roleID, unread, name, avatar, description, color, containingThreadID, sourceMessageID, repliesCount, pinnedCount, timestamps, } = input; const memberIDs = allMemberIDsWithSubscriptions.map(({ id }) => id); const threadColor = color ?? generatePendingThreadColor(memberIDs); const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while creating ` + 'thick thread but is missing from the store', ); } const { membershipPermissions, role } = createRoleAndPermissionForThickThreads( threadType, threadID, roleID, parentThreadInfo, ); const viewerIsMember = allMemberIDsWithSubscriptions.some( member => member.id === utilities.viewerID, ); const viewerRoleID = viewerIsMember ? role.id : null; const viewerMembershipPermissions = createPermissionsInfo( threadID, threadType, viewerIsMember, parentThreadInfo, ); const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, color: threadColor, creationTime, parentThreadID, members: allMemberIDsWithSubscriptions.map( ({ id: memberID, subscription }) => minimallyEncodeMemberInfo({ id: memberID, role: memberID === utilities.viewerID ? viewerRoleID : role.id, permissions: memberID === utilities.viewerID ? viewerMembershipPermissions : membershipPermissions, isSender: memberID === utilities.viewerID, subscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: viewerRoleID, permissions: viewerMembershipPermissions, subscription: joinThreadSubscription, unread, }), repliesCount: repliesCount ?? 0, name, avatar, description: description ?? '', containingThreadID, timestamps, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } function createMessageDataWithInfoFromDMOperation( dmOperation: DMCreateThreadOperation, ) { const { threadID, creatorID, time, threadType, memberIDs, newMessageID } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = generatePendingThreadColor(allMemberIDs); const messageData = { type: messageTypes.CREATE_THREAD, threadID, creatorID, time, initialThreadState: { type: threadType, color, memberIDs: allMemberIDs, }, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, newMessageID, ); return { messageData, rawMessageInfo }; } const createThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, threadType, memberIDs, roleID } = dmOperation; const { viewerID } = utilities; const allMemberIDs = [creatorID, ...memberIDs]; const allMemberIDsWithSubscriptions = allMemberIDs.map(id => ({ id, subscription: joinThreadSubscription, })); const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, allMemberIDsWithSubscriptions, roleID, unread: creatorID !== viewerID, timestamps: createThreadTimestamps(time, allMemberIDs), }, utilities, ); const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: rawThreadInfo, + }, }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMCreateThreadOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { console.log( 'Discarded a CREATE_THREAD operation because thread ' + `with the same ID ${dmOperation.threadID} already exists ` + 'in the store', ); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, operationValidator: dmCreateThreadOperationValidator, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, createPermissionsInfo, }; diff --git a/lib/shared/dm-ops/delete-entry-spec.js b/lib/shared/dm-ops/delete-entry-spec.js index f84fd4df6..9533f3b48 100644 --- a/lib/shared/dm-ops/delete-entry-spec.js +++ b/lib/shared/dm-ops/delete-entry-spec.js @@ -1,125 +1,128 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMDeleteEntryOperation, dmDeleteEntryOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMDeleteEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, prevText, messageID } = dmOperation; const messageData = { type: messageTypes.DELETE_ENTRY, threadID, creatorID, time, entryID, date: entryDate, text: prevText, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const deleteEntrySpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMDeleteEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, creationTime, entryID, entryDate: dateString, prevText, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry thread should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: utilities.threadInfos[threadID], + }, }; if (timestamp > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; } const date = dateFromString(dateString); const rawEntryInfoToUpdate = { id: entryID, threadID, text: prevText, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime, creatorID, thick: true, deleted: true, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfoToUpdate, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMDeleteEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.entryInfos[dmOperation.entryID]) { return { isProcessingPossible: false, reason: { type: 'missing_entry', entryID: dmOperation.entryID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmDeleteEntryOperationValidator, }); export { deleteEntrySpec }; diff --git a/lib/shared/dm-ops/edit-entry-spec.js b/lib/shared/dm-ops/edit-entry-spec.js index 3258f9737..4434ede24 100644 --- a/lib/shared/dm-ops/edit-entry-spec.js +++ b/lib/shared/dm-ops/edit-entry-spec.js @@ -1,126 +1,129 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMEditEntryOperation, dmEditEntryOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { EntryUpdateInfo } from '../../types/update-types.js'; import { dateFromString } from '../../utils/date-utils.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMEditEntryOperation, ) { const { threadID, creatorID, time, entryID, entryDate, text, messageID } = dmOperation; const messageData = { type: messageTypes.EDIT_ENTRY, threadID, creatorID, entryID, time, date: entryDate, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const editEntrySpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMEditEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, creationTime, time, entryID, entryDate: dateString, text, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: utilities.threadInfos[threadID], + }, }; if (timestamp > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; } const date = dateFromString(dateString); const rawEntryInfoToUpdate = { id: entryID, threadID, text, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime, creatorID, thick: true, deleted: false, lastUpdatedTime: time, }; const entryUpdateInfo: EntryUpdateInfo = { entryInfo: rawEntryInfoToUpdate, type: updateTypes.UPDATE_ENTRY, id: uuid.v4(), time, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMEditEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.entryInfos[dmOperation.entryID]) { return { isProcessingPossible: false, reason: { type: 'missing_entry', entryID: dmOperation.entryID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmEditEntryOperationValidator, }); export { editEntrySpec }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index 7fb94085e..d633dff47 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,204 +1,216 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads, createThickRawThreadInfo, } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMJoinThreadOperation, dmJoinThreadOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus, type RawMessageInfo, } from '../../types/message-types.js'; import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMJoinThreadOperation, ) { const { joinerID, time, existingThreadDetails, messageID } = dmOperation; const messageData = { type: messageTypes.JOIN_THREAD, threadID: existingThreadDetails.threadID, creatorID: joinerID, time, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const joinThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMJoinThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { joinerID, time, existingThreadDetails } = dmOperation; const { viewerID, threadInfos } = utilities; const currentThreadInfo = threadInfos[existingThreadDetails.threadID]; const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const joinThreadMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...currentThreadInfo?.timestamps?.members }; if (!memberTimestamps[joinerID]) { memberTimestamps[joinerID] = { isMember: time, subscription: existingThreadDetails.creationTime, }; } - const notificationsCreationData = { - messageDatasWithMessageInfos: [messageDataWithMessageInfos], - }; - if (memberTimestamps[joinerID].isMember > time) { + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [currentThreadInfo.id]: currentThreadInfo, + }, + }; return { rawMessageInfos: joinThreadMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; } memberTimestamps[joinerID] = { ...memberTimestamps[joinerID], isMember: time, }; const updateInfos: Array = []; const rawMessageInfos: Array = []; + let resultThreadInfo = currentThreadInfo; if (userIsMember(currentThreadInfo, joinerID)) { rawMessageInfos.push(...joinThreadMessageInfos); + resultThreadInfo = { + ...currentThreadInfo, + timestamps: { + ...currentThreadInfo.timestamps, + members: memberTimestamps, + }, + }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, - threadInfo: { - ...currentThreadInfo, - timestamps: { - ...currentThreadInfo.timestamps, - members: memberTimestamps, - }, - }, + threadInfo: resultThreadInfo, }); } else if (viewerID === joinerID) { - const newThreadInfo = createThickRawThreadInfo( + resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, { id: joinerID, subscription: joinThreadSubscription }, ], timestamps: { ...existingThreadDetails.timestamps, members: memberTimestamps, }, }, utilities, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, - threadInfo: newThreadInfo, + threadInfo: resultThreadInfo, rawMessageInfos: joinThreadMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }); } else { rawMessageInfos.push(...joinThreadMessageInfos); const defaultRoleID = values(currentThreadInfo.roles).find(role => roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); const parentThreadID = existingThreadDetails.parentThreadID; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while joining ` + 'thick thread but is missing from the store', ); } const { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, parentThreadInfo, ); const member = minimallyEncodeMemberInfo({ id: joinerID, role: defaultRoleID, permissions: membershipPermissions, isSender: joinerID === viewerID, subscription: joinThreadSubscription, }); - const updatedThreadInfo = { + resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, member], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, - threadInfo: updatedThreadInfo, + threadInfo: resultThreadInfo, }); } + + const notificationsCreationData = { + messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [resultThreadInfo.id]: resultThreadInfo, + }, + }; + return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMJoinThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { viewerID, threadInfos } = utilities; if ( threadInfos[dmOperation.existingThreadDetails.threadID] || dmOperation.joinerID === viewerID ) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.existingThreadDetails.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmJoinThreadOperationValidator, }); export { joinThreadSpec }; diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index cc1148b6b..7b0aa5e07 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,279 +1,282 @@ // @flow import uuid from 'uuid'; import { createPermissionsInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMLeaveThreadOperation, dmLeaveThreadOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { minimallyEncodeThreadCurrentUserInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { ThickRawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMLeaveThreadOperation, ) { const { editorID, time, threadID, messageID } = dmOperation; const messageData = { type: messageTypes.LEAVE_THREAD, threadID, creatorID: editorID, time, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function createDeleteSubthreadsUpdates( dmOperation: DMLeaveThreadOperation, threadInfo: ThickRawThreadInfo, threadInfos: ThickRawThreadInfos, ): Array { const updates: Array = []; for (const thread of values(threadInfos)) { if (thread.parentThreadID !== threadInfo.id) { continue; } updates.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time: dmOperation.time, threadID: thread.id, }); } return updates; } function createLeaveSubthreadsUpdates( dmOperation: DMLeaveThreadOperation, threadInfo: ThickRawThreadInfo, threadInfos: ThickRawThreadInfos, ): Array { const updates: Array = []; for (const thread of values(threadInfos)) { if (thread.parentThreadID !== threadInfo.id) { continue; } const userID = dmOperation.editorID; let userTimestamps = thread.timestamps.members[userID]; if (!userTimestamps) { userTimestamps = { isMember: thread.creationTime, subscription: thread.creationTime, }; } if (userTimestamps.isMember > dmOperation.time) { continue; } const updatedThread = { ...thread, members: thread.members.filter(member => member.id !== userID), timestamps: { ...thread.timestamps, members: { ...thread.timestamps.members, [userID]: { ...userTimestamps, isMember: dmOperation.time, }, }, }, }; updates.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time: dmOperation.time, threadInfo: updatedThread, }); } return updates; } const leaveThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMLeaveThreadOperation, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; const threadInfo = threadInfos[threadID]; const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...threadInfo.timestamps.members }; if (!memberTimestamps[editorID]) { memberTimestamps[editorID] = { isMember: time, subscription: threadInfo.creationTime, }; } memberTimestamps[editorID] = { ...memberTimestamps[editorID], isMember: time, }; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [threadID]: threadInfo, + }, }; if (viewerID === editorID) { if (threadInfo.timestamps.members[editorID]?.isMember > time) { return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; } if (threadInfo.type !== threadTypes.THICK_SIDEBAR) { return { rawMessageInfos, updateInfos: [ { type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }, ...createDeleteSubthreadsUpdates( dmOperation, threadInfo, threadInfos, ), ], blobOps: [], notificationsCreationData, }; } const parentThreadID = threadInfo.parentThreadID; const parentThreadInfo = parentThreadID ? utilities.threadInfos[parentThreadID] : null; if (parentThreadID && !parentThreadInfo) { console.log( `Parent thread with ID ${parentThreadID} was expected while ` + 'leaving a thread but is missing from the store', ); } const viewerMembershipPermissions = createPermissionsInfo( threadID, threadInfo.type, false, parentThreadInfo, ); const { minimallyEncoded, permissions, ...currentUserInfo } = threadInfo.currentUser; const currentUser = minimallyEncodeThreadCurrentUserInfo({ ...currentUserInfo, role: null, permissions: viewerMembershipPermissions, }); const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), currentUser, timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; return { rawMessageInfos, updateInfos: [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, ], blobOps: [], notificationsCreationData, }; } const updateInfos = createLeaveSubthreadsUpdates( dmOperation, threadInfo, threadInfos, ); // It is possible that the editor has joined this thread after leaving it, // but regardless, we should possibly leave the sidebars. We need to do // that because it isn't guaranteed that the editor rejoined them. if (threadInfo.timestamps.members[editorID]?.isMember > time) { return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; } const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMLeaveThreadOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmLeaveThreadOperationValidator, }); export { leaveThreadSpec }; diff --git a/lib/shared/dm-ops/send-edit-message-spec.js b/lib/shared/dm-ops/send-edit-message-spec.js index f2defaf91..c8cc69491 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,74 +1,80 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendEditMessageOperation, dmSendEditMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendEditMessageOperation, ) { const { threadID, creatorID, time, targetMessageID, text, messageID } = dmOperation; const messageData = { type: messageTypes.EDIT_MESSAGE, threadID, creatorID, time, targetMessageID, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendEditMessageSpec: DMOperationSpec = Object.freeze({ - processDMOperation: async (dmOperation: DMSendEditMessageOperation) => { + processDMOperation: async ( + dmOperation: DMSendEditMessageOperation, + utilities: ProcessDMOperationUtilities, + ) => { const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [dmOperation.threadID]: utilities.threadInfos[dmOperation.threadID], + }, }; return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendEditMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const targetMessage = await utilities.fetchMessage( dmOperation.targetMessageID, ); if (!targetMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmSendEditMessageOperationValidator, }); export { sendEditMessageSpec }; diff --git a/lib/shared/dm-ops/send-multimedia-message-spec.js b/lib/shared/dm-ops/send-multimedia-message-spec.js index 401b6c563..9b342f7a5 100644 --- a/lib/shared/dm-ops/send-multimedia-message-spec.js +++ b/lib/shared/dm-ops/send-multimedia-message-spec.js @@ -1,106 +1,110 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { encryptedMediaBlobURI, encryptedVideoThumbnailBlobURI, } from '../../media/media-utils.js'; import { type DMSendMultimediaMessageOperation, type DMBlobOperation, dmSendMultimediaMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { blobHashFromBlobServiceURI } from '../../utils/blob-service.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendMultimediaMessageOperation, ) { const { threadID, creatorID, time, media, messageID } = dmOperation; const messageData = { type: messageTypes.MULTIMEDIA, threadID, creatorID, time, media, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } function getBlobOpsFromOperation( dmOperation: DMSendMultimediaMessageOperation, ): Array { const ops: Array = []; for (const media of dmOperation.media) { if (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') { continue; } const blobURI = encryptedMediaBlobURI(media); ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(blobURI), dmOpType: 'inbound_only', }); if (media.type === 'encrypted_video') { const thumbnailBlobURI = encryptedVideoThumbnailBlobURI(media); ops.push({ type: 'establish_holder', blobHash: blobHashFromBlobServiceURI(thumbnailBlobURI), dmOpType: 'inbound_only', }); } } return ops; } const sendMultimediaMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMSendMultimediaMessageOperation, + utilities: ProcessDMOperationUtilities, ) => { const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; const blobOps = getBlobOpsFromOperation(dmOperation); const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [dmOperation.threadID]: utilities.threadInfos[dmOperation.threadID], + }, }; return { rawMessageInfos, updateInfos, blobOps, notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendMultimediaMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: false, operationValidator: dmSendMultimediaMessageOperationValidator, }); export { sendMultimediaMessageSpec }; diff --git a/lib/shared/dm-ops/send-reaction-message-spec.js b/lib/shared/dm-ops/send-reaction-message-spec.js index 6c2d9133e..8fcd3d949 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,82 +1,88 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendReactionMessageOperation, dmSendReactionMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendReactionMessageOperation, ) { const { threadID, creatorID, time, targetMessageID, reaction, action, messageID, } = dmOperation; const messageData = { type: messageTypes.REACTION, threadID, creatorID, time, targetMessageID, reaction, action, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendReactionMessageSpec: DMOperationSpec = Object.freeze({ - processDMOperation: async (dmOperation: DMSendReactionMessageOperation) => { + processDMOperation: async ( + dmOperation: DMSendReactionMessageOperation, + utilities: ProcessDMOperationUtilities, + ) => { const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [dmOperation.threadID]: utilities.threadInfos[dmOperation.threadID], + }, }; return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendReactionMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const targetMessage = await utilities.fetchMessage( dmOperation.targetMessageID, ); if (!targetMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, operationValidator: dmSendReactionMessageOperationValidator, }); export { sendReactionMessageSpec }; diff --git a/lib/shared/dm-ops/send-text-message-spec.js b/lib/shared/dm-ops/send-text-message-spec.js index b032af67a..7dcd7753b 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,69 +1,75 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMSendTextMessageOperation, dmSendTextMessageOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendTextMessageOperation, ) { const { threadID, creatorID, time, text, messageID } = dmOperation; const messageData = { type: messageTypes.TEXT, threadID, creatorID, time, text, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const sendTextMessageSpec: DMOperationSpec = Object.freeze({ - processDMOperation: async (dmOperation: DMSendTextMessageOperation) => { + processDMOperation: async ( + dmOperation: DMSendTextMessageOperation, + utilities: ProcessDMOperationUtilities, + ) => { const messageDataWithMessageInfos = createMessageDataWithInfoFromDMOperation(dmOperation); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [dmOperation.threadID]: utilities.threadInfos[dmOperation.threadID], + }, }; return { rawMessageInfos, updateInfos, blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMSendTextMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (!utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: false, operationValidator: dmSendTextMessageOperationValidator, }); export { sendTextMessageSpec }; diff --git a/lib/shared/dm-ops/update-relationship-spec.js b/lib/shared/dm-ops/update-relationship-spec.js index 19a544fd3..04fde8175 100644 --- a/lib/shared/dm-ops/update-relationship-spec.js +++ b/lib/shared/dm-ops/update-relationship-spec.js @@ -1,105 +1,108 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMUpdateRelationshipOperation, dmUpdateRelationshipOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; async function createMessageDataWithInfoFromDMOperation( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) { const { threadID, creatorID, time, operation, messageID, targetUserID } = dmOperation; const { findUserIdentities } = utilities; if (operation !== 'farcaster_mutual') { const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, targetID: targetUserID, time, operation, }; const rawMessageInfo = rawMessageInfoFromMessageData( messageData, messageID, ); return { rawMessageInfo, messageData }; } const { identities: userIdentities } = await findUserIdentities([ creatorID, targetUserID, ]); const creatorFID = userIdentities[creatorID]?.farcasterID; const targetFID = userIdentities[targetUserID]?.farcasterID; if (!creatorFID || !targetFID) { const errorMap = { [creatorID]: creatorFID, [targetUserID]: targetFID }; throw new Error( 'could not fetch FID for either creator or target: ' + JSON.stringify(errorMap), ); } const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, creatorFID, targetID: targetUserID, targetFID, time, operation, }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { rawMessageInfo, messageData }; } const updateRelationshipSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { const messageDataWithMessageInfos = await createMessageDataWithInfoFromDMOperation(dmOperation, utilities); const { rawMessageInfo } = messageDataWithMessageInfos; const rawMessageInfos = [rawMessageInfo]; const notificationsCreationData = { messageDatasWithMessageInfos: [messageDataWithMessageInfos], + thickRawThreadInfos: { + [dmOperation.threadID]: utilities.threadInfos[dmOperation.threadID], + }, }; return { rawMessageInfos, updateInfos: [], blobOps: [], notificationsCreationData, }; }, canBeProcessed: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { try { await createMessageDataWithInfoFromDMOperation(dmOperation, utilities); return { isProcessingPossible: true, }; } catch (e) { return { isProcessingPossible: false, reason: { type: 'invalid' }, }; } }, supportsAutoRetry: true, operationValidator: dmUpdateRelationshipOperationValidator, }); export { updateRelationshipSpec }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index 46001bf06..2e33b786e 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,430 +1,432 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; import type { MessageData, RawMessageInfo } from './message-types.js'; +import type { ThickRawThreadInfos } from './thread-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 = | { +messageDatasWithMessageInfos: ?$ReadOnlyArray<{ +messageData: MessageData, +rawMessageInfo: RawMessageInfo, }>, + +thickRawThreadInfos: ThickRawThreadInfos, } | { +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, +getBlobHash: (blob: Uint8Array) => Promise, +generateAESKey: () => Promise, +encryptWithAESKey: ( encryptionKey: string, unencrypotedData: string, ) => Promise, +normalizeUint8ArrayForBlobUpload: (array: Uint8Array) => string | Blob, };