diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js index 3a1e43034..e1878e97b 100644 --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -1,235 +1,236 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { preparePushNotifs, prepareOwnDevicesPushNotifs, type PerUserTargetedNotifications, } from './send-utils.js'; import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js'; import { thickRawThreadInfosSelector } from '../selectors/thread-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { TargetedAPNsNotification, TargetedAndroidNotification, TargetedWebNotification, TargetedWNSNotification, NotificationsCreationData, } from '../types/notif-types.js'; import { deviceToTunnelbrokerMessageTypes } from '../types/tunnelbroker/messages.js'; import type { TunnelbrokerAPNsNotif, TunnelbrokerFCMNotif, TunnelbrokerWebPushNotif, TunnelbrokerWNSNotif, } from '../types/tunnelbroker/notif-types.js'; import { getConfig } from '../utils/config.js'; import { getMessageForException } from '../utils/errors.js'; import { useSelector } from '../utils/redux-utils.js'; function apnsNotifToTunnelbrokerAPNsNotif( targetedNotification: TargetedAPNsNotification, ): TunnelbrokerAPNsNotif { const { deliveryID: deviceID, notification: { headers, ...payload }, } = targetedNotification; const newHeaders = { ...headers, 'apns-push-type': 'Alert', }; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_APNS_NOTIF, deviceID, headers: JSON.stringify(newHeaders), payload: JSON.stringify(payload), clientMessageID: uuid.v4(), }; } function androidNotifToTunnelbrokerFCMNotif( targetedNotification: TargetedAndroidNotification, ): TunnelbrokerFCMNotif { const { deliveryID: deviceID, notification: { data }, priority, } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_FCM_NOTIF, deviceID, clientMessageID: uuid.v4(), data: JSON.stringify(data), priority: priority === 'normal' ? 'NORMAL' : 'HIGH', }; } function webNotifToTunnelbrokerWebPushNotif( targetedNotification: TargetedWebNotification, ): TunnelbrokerWebPushNotif { const { deliveryID: deviceID, notification } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WEB_PUSH_NOTIF, deviceID, clientMessageID: uuid.v4(), payload: JSON.stringify(notification), }; } function wnsNotifToTunnelbrokerWNSNofif( targetedNotification: TargetedWNSNotification, ): TunnelbrokerWNSNotif { const { deliveryID: deviceID, notification } = targetedNotification; return { type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WNS_NOTIF, deviceID, clientMessageID: uuid.v4(), payload: JSON.stringify(notification), }; } function useSendPushNotifs(): ( notifCreationData: ?NotificationsCreationData, ) => Promise { const client = React.useContext(IdentityClientContext); invariant(client, 'Identity context should be set'); const { getAuthMetadata } = client; const rawMessageInfos = useSelector(state => state.messageStore.messages); const thickRawThreadInfos = useSelector(thickRawThreadInfosSelector); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); const userInfos = useSelector(state => state.userStore.userInfos); const { getENSNames } = React.useContext(ENSCacheContext); const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; const { createOlmSessionsWithUser: olmSessionCreator } = usePeerOlmSessionsCreatorContext(); const { sendNotif } = useTunnelbroker(); const { encryptedNotifUtilsAPI } = getConfig(); return React.useCallback( async (notifCreationData: ?NotificationsCreationData) => { if (!notifCreationData) { return; } const { deviceID, userID: senderUserID } = await getAuthMetadata(); if (!deviceID || !senderUserID) { return; } const senderDeviceDescriptor = { senderDeviceID: deviceID }; const senderInfo = { senderUserID, senderDeviceDescriptor, }; - const { messageDatas, rescindData, badgeUpdateData } = notifCreationData; + const { messageDatasWithMessageInfos, rescindData, badgeUpdateData } = + notifCreationData; const pushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, messageInfos: rawMessageInfos, thickRawThreadInfos, auxUserInfos, - messageDatas, + 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 = preparedPushNotifs; if (preparedOwnDevicesPushNotifs && senderUserID) { allPreparedPushNotifs = { ...allPreparedPushNotifs, [senderUserID]: preparedOwnDevicesPushNotifs, }; } const sendPromises = []; for (const userID in allPreparedPushNotifs) { for (const notif of allPreparedPushNotifs[userID]) { if (notif.targetedNotification.notification.encryptionFailed) { continue; } let tunnelbrokerNotif; if (notif.platform === 'ios' || notif.platform === 'macos') { tunnelbrokerNotif = apnsNotifToTunnelbrokerAPNsNotif( notif.targetedNotification, ); } else if (notif.platform === 'android') { tunnelbrokerNotif = androidNotifToTunnelbrokerFCMNotif( notif.targetedNotification, ); } else if (notif.platform === 'web') { tunnelbrokerNotif = webNotifToTunnelbrokerWebPushNotif( notif.targetedNotification, ); } else if (notif.platform === 'windows') { tunnelbrokerNotif = wnsNotifToTunnelbrokerWNSNofif( notif.targetedNotification, ); } else { continue; } sendPromises.push( (async () => { try { await sendNotif(tunnelbrokerNotif); } catch (e) { console.log( `Failed to send notification to device: ${ tunnelbrokerNotif.deviceID }. Details: ${getMessageForException(e) ?? ''}`, ); } })(), ); } } await Promise.all(sendPromises); }, [ getAuthMetadata, sendNotif, encryptedNotifUtilsAPI, olmSessionCreator, rawMessageInfos, thickRawThreadInfos, auxUserInfos, userInfos, getENSNames, getFCNames, ], ); } export { useSendPushNotifs }; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js index 275705753..6a4709157 100644 --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -1,1179 +1,1180 @@ // @flow import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; import { createAndroidVisualNotification, createAndroidBadgeOnlyNotification, createAndroidNotificationRescind, } from './android-notif-creators.js'; import { createAPNsVisualNotification, createAPNsBadgeOnlyNotification, createAPNsNotificationRescind, } from './apns-notif-creators.js'; import { stringToVersionKey, getDevicesByPlatform, generateNotifUserInfoPromise, userAllowsNotif, } from './utils.js'; import { createWebNotification } from './web-notif-creators.js'; import { createWNSNotification } from './wns-notif-creators.js'; import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js'; import { - rawMessageInfoFromMessageData, createMessageInfo, shimUnsupportedRawMessageInfos, sortMessageInfoList, } from '../shared/message-utils.js'; import { pushTypes } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import { notifTextsForMessageInfo, getNotifCollapseKey, } from '../shared/notif-utils.js'; import { isMemberActive, threadInfoFromRawThreadInfo, } from '../shared/thread-utils.js'; import { hasMinCodeVersion } from '../shared/version-utils.js'; import type { AuxUserInfos } from '../types/aux-user-types.js'; import type { PlatformDetails, Platform } from '../types/device-types.js'; import { identityDeviceTypeToPlatform, type IdentityPlatformDetails, } from '../types/identity-service-types.js'; import { type MessageData, type RawMessageInfo, - messageDataLocalID, } from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { ResolvedNotifTexts, NotificationTargetDevice, TargetedNotificationWithPlatform, SenderDeviceDescriptor, EncryptedNotifUtilsAPI, } from '../types/notif-types.js'; import type { ThreadSubscription } from '../types/subscription-types.js'; import type { ThickRawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; import type { 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, - messageDatas: ?$ReadOnlyArray, + messageDataWithMessageInfos: ?$ReadOnlyArray<{ + +messageData: MessageData, + +rawMessageInfo: RawMessageInfo, + }>, ): Promise<{ +pushInfos: ?PushInfo, +rescindInfos: ?PushInfo, }> { - if (!messageDatas || messageDatas.length === 0) { + if (!messageDataWithMessageInfos) { return { pushInfos: null, rescindInfos: null }; } const threadsToMessageIndices: Map = new Map(); const newMessageInfos: RawMessageInfo[] = []; + const messageDatas: MessageData[] = []; let nextNewMessageIndex = 0; - for (let i = 0; i < messageDatas.length; i++) { - const messageData = messageDatas[i]; + for (const messageDataWithInfo of messageDataWithMessageInfos) { + 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); - - const messageID = messageDataLocalID(messageData) ?? uuidv4(); - const rawMessageInfo = rawMessageInfoFromMessageData( - messageData, - messageID, - ); + 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), rescindInfos: _pickBy(Boolean)(rescindInfo), }; } type SenderInfo = { +senderUserID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, }; type OwnDevicesPushInfo = { +devices: $ReadOnlyArray, }; function getOwnDevicesPushInfo( senderInfo: SenderInfo, auxUserInfos: AuxUserInfos, ): ?OwnDevicesPushInfo { const { senderUserID, senderDeviceDescriptor: { senderDeviceID }, } = senderInfo; if (!senderDeviceID) { return null; } const senderDevicesWithPlatformDetails = auxUserInfos[senderUserID].devicesPlatformDetails; if (!senderDevicesWithPlatformDetails) { return null; } const devices = Object.entries(senderDevicesWithPlatformDetails) .filter(([deviceID]) => deviceID !== senderDeviceID) .map(([deviceID, identityPlatformDetails]) => ({ platformDetails: identityPlatformDetailsToPlatformDetails( identityPlatformDetails, ), deliveryID: deviceID, cryptoID: deviceID, })); return { devices }; } function pushInfoToCollapsableNotifInfo(pushInfo: PushInfo): { +usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, +usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, } { const usersToCollapseKeysToInfo: { [string]: { [string]: CollapsableNotifInfo }, } = {}; const usersToCollapsableNotifInfo: { [string]: Array } = {}; for (const userID in pushInfo) { usersToCollapseKeysToInfo[userID] = {}; usersToCollapsableNotifInfo[userID] = []; for (let i = 0; i < pushInfo[userID].messageInfos.length; i++) { const rawMessageInfo = pushInfo[userID].messageInfos[i]; const messageData = pushInfo[userID].messageDatas[i]; const collapseKey = getNotifCollapseKey(rawMessageInfo, messageData); if (!collapseKey) { const collapsableNotifInfo: CollapsableNotifInfo = { collapseKey, existingMessageInfos: [], newMessageInfos: [rawMessageInfo], }; usersToCollapsableNotifInfo[userID].push(collapsableNotifInfo); continue; } if (!usersToCollapseKeysToInfo[userID][collapseKey]) { usersToCollapseKeysToInfo[userID][collapseKey] = ({ collapseKey, existingMessageInfos: [], newMessageInfos: [], }: CollapsableNotifInfo); } usersToCollapseKeysToInfo[userID][collapseKey].newMessageInfos.push( rawMessageInfo, ); } } return { usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, }; } function mergeUserToCollapsableInfo( usersToCollapseKeysToInfo: { +[string]: { +[string]: CollapsableNotifInfo }, }, usersToCollapsableNotifInfo: { +[string]: $ReadOnlyArray, }, ): { +[string]: $ReadOnlyArray } { const mergedUsersToCollapsableInfo: { [string]: Array, } = {}; for (const userID in usersToCollapseKeysToInfo) { const collapseKeysToInfo = usersToCollapseKeysToInfo[userID]; mergedUsersToCollapsableInfo[userID] = [ ...usersToCollapsableNotifInfo[userID], ]; for (const collapseKey in collapseKeysToInfo) { const info = collapseKeysToInfo[collapseKey]; mergedUsersToCollapsableInfo[userID].push({ collapseKey: info.collapseKey, existingMessageInfos: sortMessageInfoList(info.existingMessageInfos), newMessageInfos: sortMessageInfoList(info.newMessageInfos), }); } } return mergedUsersToCollapsableInfo; } async function buildNotifText( rawMessageInfos: $ReadOnlyArray, userID: string, threadInfos: { +[id: string]: ThreadInfo }, subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, +badgeOnly: boolean, }> { if (!subscriptions) { return null; } const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); const newMessageInfos = []; const newRawMessageInfos = []; for (const rawMessageInfo of rawMessageInfos) { const newMessageInfo = hydrateMessageInfo(rawMessageInfo); if (newMessageInfo) { newMessageInfos.push(newMessageInfo); newRawMessageInfos.push(rawMessageInfo); } } if (newMessageInfos.length === 0) { return null; } const [{ threadID }] = newMessageInfos; const threadInfo = threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; const subscription = subscriptions[threadID]; if (!subscription) { return null; } const username = userInfos[userID] && userInfos[userID].username; const { notifAllowed, badgeOnly } = await userAllowsNotif({ subscription, userID, newMessageInfos, userInfos, username, getENSNames, }); if (!notifAllowed) { return null; } const notifTargetUserInfo = { id: userID, username }; const notifTexts = await notifTextsForMessageInfo( newMessageInfos, threadInfo, parentThreadInfo, notifTargetUserInfo, getENSNames, getFCNames, ); if (!notifTexts) { return null; } return { notifTexts, newRawMessageInfos, badgeOnly }; } type BuildNotifsForPlatformInput< PlatformType: Platform, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput: { +platformDetails: PlatformDetails, ... }, > = { +platform: PlatformType, +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +notifCreatorCallback: ( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, input: NotifCreatorInput, devices: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>, +notifCreatorInputBase: NotifCreatorinputBase, +transformInputBase: ( inputBase: NotifCreatorinputBase, platformDetails: PlatformDetails, ) => NotifCreatorInput, +versionToDevices: $ReadOnlyMap< string, $ReadOnlyArray, >, }; async function buildNotifsForPlatform< PlatformType: Platform, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput: { +platformDetails: PlatformDetails, ... }, >( input: BuildNotifsForPlatformInput< PlatformType, NotifCreatorinputBase, TargetedNotificationType, NotifCreatorInput, >, ): Promise< $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, > { const { encryptedNotifUtilsAPI, versionToDevices, notifCreatorCallback, notifCreatorInputBase, platform, transformInputBase, } = input; const promises: Array< Promise< $ReadOnlyArray<{ +platform: PlatformType, +targetedNotification: TargetedNotificationType, }>, >, > = []; for (const [versionKey, devices] of versionToDevices) { const { codeVersion, stateVersion, majorDesktopVersion } = stringToVersionKey(versionKey); const platformDetails = { platform, codeVersion, stateVersion, majorDesktopVersion, }; const inputData = transformInputBase( notifCreatorInputBase, platformDetails, ); promises.push( (async () => { return ( await notifCreatorCallback(encryptedNotifUtilsAPI, inputData, devices) ).map(targetedNotification => ({ platform, targetedNotification, })); })(), ); } return (await Promise.all(promises)).flat(); } type BuildNotifsForUserDevicesInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +rawMessageInfos: $ReadOnlyArray, +userID: string, +threadInfos: { +[id: string]: ThreadInfo }, +subscriptions: ?{ +[threadID: string]: ThreadSubscriptionWithRole }, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, +devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, }; async function buildNotifsForUserDevices( inputData: BuildNotifsForUserDevicesInputData, ): Promise> { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, rawMessageInfos, userID, threadInfos, subscriptions, userInfos, getENSNames, getFCNames, devicesByPlatform, } = inputData; const notifTextWithNewRawMessageInfos = await buildNotifText( rawMessageInfos, userID, threadInfos, subscriptions, userInfos, getENSNames, getFCNames, ); if (!notifTextWithNewRawMessageInfos) { return null; } const { notifTexts, newRawMessageInfos, badgeOnly } = notifTextWithNewRawMessageInfos; const [{ threadID }] = newRawMessageInfos; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, notifID: uuidv4(), }, versionToDevices: androidVersionToDevices, }), ); } const macosVersionToDevices = devicesByPlatform.get('macos'); if (macosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'macos', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsVisualNotification, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, newRawMessageInfos: shimUnsupportedRawMessageInfos( newRawMessageInfos, platformDetails, ), platformDetails, }), notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, collapseKey: undefined, badgeOnly, uniqueID: uuidv4(), }, versionToDevices: macosVersionToDevices, }), ); } const windowsVersionToDevices = devicesByPlatform.get('windows'); if (windowsVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'windows', encryptedNotifUtilsAPI, notifCreatorCallback: createWNSNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: windowsVersionToDevices, }), ); } const webVersionToDevices = devicesByPlatform.get('web'); if (webVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'web', encryptedNotifUtilsAPI, notifCreatorCallback: createWebNotification, notifCreatorInputBase: { senderDeviceDescriptor, notifTexts, threadID, id: uuidv4(), }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: webVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } async function buildRescindsForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, rescindData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = rescindData; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsNotificationRescind, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidNotificationRescind, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: androidVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } async function buildBadgeUpdatesForOwnDevices( encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, senderDeviceDescriptor: SenderDeviceDescriptor, devicesByPlatform: $ReadOnlyMap< Platform, $ReadOnlyMap>, >, badgeUpdateData: { +threadID: string }, ): Promise<$ReadOnlyArray> { const { threadID } = badgeUpdateData; const promises: Array< Promise<$ReadOnlyArray>, > = []; const iosVersionToDevices = devicesByPlatform.get('ios'); if (iosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'ios', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: iosVersionToDevices, }), ); } const androidVersionToDevices = devicesByPlatform.get('android'); if (androidVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'android', encryptedNotifUtilsAPI, notifCreatorCallback: createAndroidBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: androidVersionToDevices, }), ); } const macosVersionToDevices = devicesByPlatform.get('macos'); if (macosVersionToDevices) { promises.push( buildNotifsForPlatform({ platform: 'macos', encryptedNotifUtilsAPI, notifCreatorCallback: createAPNsBadgeOnlyNotification, notifCreatorInputBase: { senderDeviceDescriptor, threadID, }, transformInputBase: (inputBase, platformDetails) => ({ ...inputBase, platformDetails, }), versionToDevices: macosVersionToDevices, }), ); } return (await Promise.all(promises)).flat(); } export type PerUserTargetedNotifications = { +[userID: string]: $ReadOnlyArray, }; type BuildNotifsFromPushInfoInputData = { +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, +senderDeviceDescriptor: SenderDeviceDescriptor, +pushInfo: PushInfo, +thickRawThreadInfos: ThickRawThreadInfos, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function buildNotifsFromPushInfo( inputData: BuildNotifsFromPushInfoInputData, ): Promise { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, pushInfo, thickRawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const threadIDs = new Set(); for (const userID in pushInfo) { for (const rawMessageInfo of pushInfo[userID].messageInfos) { const threadID = rawMessageInfo.threadID; threadIDs.add(threadID); const messageSpec = messageSpecs[rawMessageInfo.type]; if (messageSpec.threadIDs) { for (const id of messageSpec.threadIDs(rawMessageInfo)) { threadIDs.add(id); } } } } const perUserBuildNotifsResultPromises: { [userID: string]: Promise<$ReadOnlyArray>, } = {}; const { usersToCollapsableNotifInfo, usersToCollapseKeysToInfo } = pushInfoToCollapsableNotifInfo(pushInfo); const mergedUsersToCollapsableInfo = mergeUserToCollapsableInfo( usersToCollapseKeysToInfo, usersToCollapsableNotifInfo, ); for (const userID in mergedUsersToCollapsableInfo) { const threadInfos = Object.fromEntries( [...threadIDs].map(threadID => [ threadID, threadInfoFromRawThreadInfo( thickRawThreadInfos[threadID], userID, userInfos, ), ]), ); const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices); const singleNotificationPromises = []; for (const notifInfo of mergedUsersToCollapsableInfo[userID]) { singleNotificationPromises.push( // We always pass one element array here // because coalescing is not supported for // notifications generated on the client buildNotifsForUserDevices({ encryptedNotifUtilsAPI, senderDeviceDescriptor, rawMessageInfos: notifInfo.newMessageInfos, userID, threadInfos, subscriptions: pushInfo[userID].subscriptions, userInfos, getENSNames, getFCNames, devicesByPlatform, }), ); } perUserBuildNotifsResultPromises[userID] = (async () => { const singleNotificationResults = await Promise.all( singleNotificationPromises, ); return singleNotificationResults.filter(Boolean).flat(); })(); } return promiseAll(perUserBuildNotifsResultPromises); } async function createOlmSessionWithDevices( 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 { await Promise.allSettled(olmSessionCreationPromises); } catch (e) { // session creation may fail for some devices // but we should still pursue notification // delivery for others console.log(e); } } function filterDevicesSupportingDMNotifs< T: { +devices: $ReadOnlyArray, ... }, >(devicesContainer: T): T { return { ...devicesContainer, devices: devicesContainer.devices.filter(({ platformDetails }) => hasMinCodeVersion(platformDetails, { native: 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, - +messageDatas: ?$ReadOnlyArray, + +messageDatasWithMessageInfos: ?$ReadOnlyArray<{ + +messageData: MessageData, + +rawMessageInfo: RawMessageInfo, + }>, +userInfos: UserInfos, +getENSNames: ?GetENSNames, +getFCNames: ?GetFCNames, }; async function preparePushNotifs( inputData: PreparePushNotifsInputData, ): Promise { const { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, - messageDatas, + messageDatasWithMessageInfos, messageInfos, auxUserInfos, thickRawThreadInfos, userInfos, getENSNames, getFCNames, } = inputData; const { pushInfos } = await getPushUserInfo( messageInfos, thickRawThreadInfos, auxUserInfos, - messageDatas, + messageDatasWithMessageInfos, ); if (!pushInfos) { return null; } const filteredPushInfos = filterDevicesSupportingDMNotifsForUsers(pushInfos); 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 0d3b6f643..f0c06a1b9 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,145 +1,155 @@ // @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 } 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 { joinThreadSubscription } from '../../types/subscription-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 createAddNewMembersMessageDataFromDMOperation( +function createAddNewMembersMessageDataWithInfoFromDMOperation( dmOperation: DMAddMembersOperation, -): AddMembersMessageData { - const { editorID, time, addedUserIDs, threadID } = dmOperation; - return { +): { + +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 }; } const addMembersSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async (dmOperation: DMAddMembersOperation) => { - const messageData = - createAddNewMembersMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { - const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; + const { editorID, time, addedUserIDs, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; - const messageData = - createAddNewMembersMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; + const currentThreadInfo = threadInfos[threadID]; if (!currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], }; } const defaultRoleID = values(currentThreadInfo.roles).find(role => roleIsDefaultRole(role), )?.id; invariant(defaultRoleID, 'Default role ID must exist'); const { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, ); 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: defaultRoleID, 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, }, ]; return { rawMessageInfos, updateInfos, }; }, 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, }); -export { addMembersSpec, createAddNewMembersMessageDataFromDMOperation }; +export { + addMembersSpec, + createAddNewMembersMessageDataWithInfoFromDMOperation, +}; 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 af5680a75..83ebfe056 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,151 +1,160 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMAddViewerToThreadMembersOperation } 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 { joinThreadSubscription } from '../../types/subscription-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { userIsMember } from '../thread-utils.js'; -function createAddViewerToThreadMembersMessageDataFromDMOp( +function createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( dmOperation: DMAddViewerToThreadMembersOperation, -): AddMembersMessageData { - const { editorID, time, addedUserIDs, existingThreadDetails } = dmOperation; - return { +): { + +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({ notificationsCreationData: async ( dmOperation: DMAddViewerToThreadMembersOperation, ) => { - const messageData = - createAddViewerToThreadMembersMessageDataFromDMOp(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( + dmOperation, + ), + ], + }; }, processDMOperation: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { time, messageID, addedUserIDs, existingThreadDetails } = dmOperation; const { viewerID, threadInfos } = utilities; - const messageData = - createAddViewerToThreadMembersMessageDataFromDMOp(dmOperation); - const rawMessageInfos = messageID - ? [rawMessageInfoFromMessageData(messageData, messageID)] - : []; + const { rawMessageInfo } = + createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation); + const rawMessageInfos = messageID ? [rawMessageInfo] : []; const threadID = existingThreadDetails.threadID; const currentThreadInfo = threadInfos[threadID]; if (currentThreadInfo && !currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], }; } 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 resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, ...newMembers.map(id => ({ id, subscription: joinThreadSubscription, })), ], timestamps: { ...existingThreadDetails.timestamps, members: { ...existingThreadDetails.timestamps.members, ...memberTimestamps, }, }, }, viewerID, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; return { rawMessageInfos, updateInfos }; }, 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, }); export { addViewerToThreadMembersSpec, - createAddViewerToThreadMembersMessageDataFromDMOp, + createAddViewerToThreadMembersMessageDataWithInfoFromDMOp, }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 0c7093d87..b7461b353 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,181 +1,184 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsOperation, DMThreadSettingsChanges, } from '../../types/dm-ops.js'; -import type { MessageData, RawMessageInfo } from '../../types/message-types'; +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 { 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]: ChangeSettingsMessageData }, + +fieldNameToMessageData: { + +[fieldName: string]: { + +messageData: ChangeSettingsMessageData, + +rawMessageInfo: RawMessageInfo, + }, + }, +threadInfoUpdate: DMThreadSettingsChanges, } { - const { changes, editorID, time } = dmOperation; + 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]: ChangeSettingsMessageData, + [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]; - fieldNameToMessageData[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 }; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMChangeThreadSettingsOperation, ) => { - const messageDatas: Array = []; - const { fieldNameToMessageData } = createChangeSettingsMessageDatasAndUpdate(dmOperation); - messageDatas.push(...values(fieldNameToMessageData)); - return { messageDatas }; + + return { messageDatasWithMessageInfos: values(fieldNameToMessageData) }; }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { - const { time, messageIDsPrefix } = dmOperation; + const { time } = dmOperation; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfo: ?RawThreadInfo = utilities.threadInfos[threadID]; const updateInfos: Array = []; - const rawMessageInfos: Array = []; const { fieldNameToMessageData, threadInfoUpdate } = createChangeSettingsMessageDatasAndUpdate(dmOperation); - const fieldNameToMessageDataPairs = Object.entries( - fieldNameToMessageData, - ); - rawMessageInfos.push( - ...fieldNameToMessageDataPairs.map(([fieldName, messageData]) => - rawMessageInfoFromMessageData( - messageData, - `${messageIDsPrefix}/${fieldName}`, - ), - ), + const messageDataWithMessageInfoPairs = values(fieldNameToMessageData); + const rawMessageInfos = messageDataWithMessageInfoPairs.map( + ({ rawMessageInfo }) => rawMessageInfo, ); invariant(threadInfo?.thick, 'Thread should be thick'); 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 (fieldNameToMessageDataPairs.length > 0) { + if (messageDataWithMessageInfoPairs.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } return { rawMessageInfos, updateInfos, }; }, canBeProcessed: async ( dmOperation: DMChangeThreadSettingsOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); export { changeThreadSettingsSpec, createChangeSettingsMessageDatasAndUpdate }; diff --git a/lib/shared/dm-ops/create-entry-spec.js b/lib/shared/dm-ops/create-entry-spec.js index 131d6f153..bb323911e 100644 --- a/lib/shared/dm-ops/create-entry-spec.js +++ b/lib/shared/dm-ops/create-entry-spec.js @@ -1,89 +1,95 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMCreateEntryOperation } 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 createMessageDataFromDMOperation(dmOperation: DMCreateEntryOperation) { - const { threadID, creatorID, time, entryID, entryDate, text } = dmOperation; - return { +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({ notificationsCreationData: async (dmOperation: DMCreateEntryOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async (dmOperation: DMCreateEntryOperation) => { - const { threadID, creatorID, time, entryID, entryDate, text, messageID } = - dmOperation; + const { threadID, creatorID, time, entryID, entryDate, text } = dmOperation; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + 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, }; return { rawMessageInfos, updateInfos: [entryUpdateInfo], }; }, canBeProcessed: async ( dmOperation: DMCreateEntryOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); export { createEntrySpec }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index b45f1f131..4c99d8be5 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,190 +1,207 @@ // @flow import uuid from 'uuid'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMCreateSidebarOperation } 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 createMessageDatasFromDMOperation( +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({ notificationsCreationData: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { - const { sidebarSourceMessageData, createSidebarMessageData } = - await createMessageDatasFromDMOperation(dmOperation, utilities); + const { + sidebarSourceMessageData, + createSidebarMessageData, + createSidebarMessageInfo, + sidebarSourceMessageInfo, + } = await createMessageDatasWithInfosFromDMOperation( + dmOperation, + utilities, + ); return { - messageDatas: [sidebarSourceMessageData, createSidebarMessageData], + messageDatasWithMessageInfos: [ + { + messageData: sidebarSourceMessageData, + rawMessageInfo: sidebarSourceMessageInfo, + }, + { + messageData: createSidebarMessageData, + rawMessageInfo: createSidebarMessageInfo, + }, + ], }; }, processDMOperation: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, roleID, - newSidebarSourceMessageID, - newCreateSidebarMessageID, } = 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), }, viewerID, ); - const { sidebarSourceMessageData, createSidebarMessageData } = - await createMessageDatasFromDMOperation( + const { sidebarSourceMessageInfo, createSidebarMessageInfo } = + await createMessageDatasWithInfosFromDMOperation( dmOperation, utilities, rawThreadInfo.color, ); - const sidebarSourceMessageInfo = rawMessageInfoFromMessageData( - sidebarSourceMessageData, - newSidebarSourceMessageID, - ); - const createSidebarMessageInfo = rawMessageInfoFromMessageData( - createSidebarMessageData, - newCreateSidebarMessageID, - ); - const rawMessageInfos: Array = [ sidebarSourceMessageInfo, createSidebarMessageInfo, ]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, canBeProcessed: async ( dmOperation: DMCreateSidebarOperation, utilities: ProcessDMOperationUtilities, ) => { const sourceMessage = await utilities.fetchMessage( dmOperation.sourceMessageID, ); if (!sourceMessage) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.sourceMessageID, }, }; } return { isProcessingPossible: true }; }, supportsAutoRetry: true, }); export { createSidebarSpec }; diff --git a/lib/shared/dm-ops/create-thread-spec.js b/lib/shared/dm-ops/create-thread-spec.js index e5df83a0b..34ec47344 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,223 +1,224 @@ // @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, } from '../../permissions/thread-permissions.js'; import type { CreateThickRawThreadInfoInput, DMCreateThreadOperation, } 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 createRoleAndPermissionForThickThreads( threadType: ThickThreadType, threadID: string, roleID: string, ): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } { const rolePermissions = getThickThreadRolePermissionsBlob(threadType); const membershipPermissions = getAllThreadPermissions( makePermissionsBlob(rolePermissions, null, threadID, threadType), threadID, ); 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, viewerID: string, ): 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 { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); 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: role.id, permissions: membershipPermissions, isSender: memberID === viewerID, subscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: role.id, permissions: membershipPermissions, subscription: joinThreadSubscription, unread, }), repliesCount: repliesCount ?? 0, name, avatar, description, containingThreadID, timestamps, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } -function createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMCreateThreadOperation, ) { - const { threadID, creatorID, time, threadType, memberIDs } = dmOperation; + const { threadID, creatorID, time, threadType, memberIDs, newMessageID } = + dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const color = generatePendingThreadColor(allMemberIDs); - return { + 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({ notificationsCreationData: async (dmOperation: DMCreateThreadOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMCreateThreadOperation, utilities: ProcessDMOperationUtilities, ) => { - const { - threadID, - creatorID, - time, - threadType, - memberIDs, - roleID, - newMessageID, - } = dmOperation; + 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), }, viewerID, ); - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, newMessageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; const threadJoinUpdateInfo = { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: rawThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }; return { rawMessageInfos: [], // included in updateInfos below updateInfos: [threadJoinUpdateInfo], }; }, canBeProcessed: async () => { return { isProcessingPossible: true }; }, supportsAutoRetry: true, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, }; diff --git a/lib/shared/dm-ops/delete-entry-spec.js b/lib/shared/dm-ops/delete-entry-spec.js index c52551c4c..5ce5ffb48 100644 --- a/lib/shared/dm-ops/delete-entry-spec.js +++ b/lib/shared/dm-ops/delete-entry-spec.js @@ -1,114 +1,119 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMDeleteEntryOperation } 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 createMessageDataFromDMOperation(dmOperation: DMDeleteEntryOperation) { - const { threadID, creatorID, time, entryID, entryDate, prevText } = +function createMessageDataWithInfoFromDMOperation( + dmOperation: DMDeleteEntryOperation, +) { + const { threadID, creatorID, time, entryID, entryDate, prevText, messageID } = dmOperation; - return { + 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({ notificationsCreationData: async (dmOperation: DMDeleteEntryOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMDeleteEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, creationTime, entryID, entryDate: dateString, prevText, - messageID, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry thread should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; if (timestamp > time) { return { rawMessageInfos, updateInfos: [], }; } 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], }; }, 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, }); export { deleteEntrySpec }; diff --git a/lib/shared/dm-ops/edit-entry-spec.js b/lib/shared/dm-ops/edit-entry-spec.js index 8b37f4532..fc7ad6b07 100644 --- a/lib/shared/dm-ops/edit-entry-spec.js +++ b/lib/shared/dm-ops/edit-entry-spec.js @@ -1,114 +1,120 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMEditEntryOperation } 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 createMessageDataFromDMOperation(dmOperation: DMEditEntryOperation) { - const { threadID, creatorID, time, entryID, entryDate, text } = dmOperation; - return { +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({ notificationsCreationData: async (dmOperation: DMEditEntryOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMEditEntryOperation, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, creationTime, time, entryID, entryDate: dateString, text, - messageID, } = dmOperation; const rawEntryInfo = utilities.entryInfos[entryID]; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; invariant(rawEntryInfo?.thick, 'Entry thread should be thick'); const timestamp = rawEntryInfo.lastUpdatedTime; if (timestamp > time) { return { rawMessageInfos, updateInfos: [], }; } 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], }; }, 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, }); export { editEntrySpec }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index 37f9a4af6..0cf8d8074 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,186 +1,191 @@ // @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 } 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 createMessageDataFromDMOperation(dmOperation: DMJoinThreadOperation) { - const { joinerID, time, existingThreadDetails } = dmOperation; - return { +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({ notificationsCreationData: async (dmOperation: DMJoinThreadOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMJoinThreadOperation, utilities: ProcessDMOperationUtilities, ) => { - const { joinerID, time, messageID, existingThreadDetails } = dmOperation; + const { joinerID, time, existingThreadDetails } = dmOperation; const { viewerID, threadInfos } = utilities; - const currentThreadInfo = threadInfos[existingThreadDetails.threadID]; if (currentThreadInfo && !currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], }; } - const messageData = createMessageDataFromDMOperation(dmOperation); - const joinThreadMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const joinThreadMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...currentThreadInfo?.timestamps?.members }; if (!memberTimestamps[joinerID]) { memberTimestamps[joinerID] = { isMember: time, subscription: existingThreadDetails.creationTime, }; } if (memberTimestamps[joinerID].isMember > time) { return { rawMessageInfos: joinThreadMessageInfos, updateInfos: [], }; } memberTimestamps[joinerID] = { ...memberTimestamps[joinerID], isMember: time, }; const updateInfos: Array = []; const rawMessageInfos: Array = []; if (userIsMember(currentThreadInfo, joinerID)) { rawMessageInfos.push(...joinThreadMessageInfos); updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: { ...currentThreadInfo, timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }, }); } else if (viewerID === joinerID) { const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, { id: joinerID, subscription: joinThreadSubscription }, ], timestamps: { ...existingThreadDetails.timestamps, members: memberTimestamps, }, }, viewerID, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, 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 { membershipPermissions } = createRoleAndPermissionForThickThreads( currentThreadInfo.type, currentThreadInfo.id, defaultRoleID, ); const member = minimallyEncodeMemberInfo({ id: joinerID, role: defaultRoleID, permissions: membershipPermissions, isSender: joinerID === viewerID, subscription: joinThreadSubscription, }); const updatedThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, member], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos, updateInfos, }; }, 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, }); export { joinThreadSpec }; diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js index 9bfbaabca..c86cf76e3 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,155 +1,160 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMLeaveThreadOperation } 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 { threadTypes } from '../../types/thread-types-enum.js'; import type { RawThreadInfos } 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 { userIsMember } from '../thread-utils.js'; -function createMessageDataFromDMOperation(dmOperation: DMLeaveThreadOperation) { - const { editorID, time, threadID } = dmOperation; - return { +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 createLeaveThreadSubthreadsUpdates( dmOperation: DMLeaveThreadOperation, threadInfo: ThickRawThreadInfo, viewerID: string, threadInfos: RawThreadInfos, ): $ReadOnlyArray { const updates = []; 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; } const leaveThreadSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async (dmOperation: DMLeaveThreadOperation) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMLeaveThreadOperation, utilities: ProcessDMOperationUtilities, ) => { - const { editorID, time, messageID, threadID } = dmOperation; + const { editorID, time, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; - const threadInfo = threadInfos[threadID]; invariant(threadInfo.thick, 'Thread should be thick'); - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; if ( viewerID === editorID && userIsMember(threadInfo, editorID) && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !threadInfos[threadInfo.parentThreadID])) ) { return { rawMessageInfos, updateInfos: [ { type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }, ...createLeaveThreadSubthreadsUpdates( dmOperation, threadInfo, viewerID, threadInfos, ), ], }; } if (threadInfo.timestamps.members[editorID]?.isMember > time) { return { rawMessageInfos, updateInfos: [], }; } const memberTimestamps = { ...threadInfo.timestamps.members }; if (!memberTimestamps[editorID]) { memberTimestamps[editorID] = { isMember: time, subscription: threadInfo.creationTime, }; } memberTimestamps[editorID] = { ...memberTimestamps[editorID], isMember: time, }; const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; return { rawMessageInfos, updateInfos: [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }, ], }; }, 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, }); export { leaveThreadSpec }; diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js index 4bf22718e..734afd70f 100644 --- a/lib/shared/dm-ops/remove-members-spec.js +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -1,130 +1,133 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMRemoveMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMRemoveMembersOperation, ) { - const { editorID, time, threadID, removedUserIDs } = dmOperation; - return { + const { editorID, time, threadID, removedUserIDs, messageID } = dmOperation; + const messageData = { type: messageTypes.REMOVE_MEMBERS, threadID, time, creatorID: editorID, removedUserIDs: [...removedUserIDs], }; + const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); + return { messageData, rawMessageInfo }; } const removeMembersSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMRemoveMembersOperation, ) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMRemoveMembersOperation, utilities: ProcessDMOperationUtilities, ) => { - const { time, messageID, threadID, removedUserIDs } = dmOperation; + const { time, threadID, removedUserIDs } = dmOperation; const { viewerID, threadInfos } = utilities; - const threadInfo = threadInfos[threadID]; invariant(threadInfo.thick, 'Thread should be thick'); - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; const memberTimestamps = { ...threadInfo.timestamps.members }; const removedUserIDsSet = new Set(); for (const userID of removedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: threadInfo.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; removedUserIDsSet.add(userID); } const viewerIsRemoved = removedUserIDsSet.has(viewerID); const updateInfos: Array = []; if ( viewerIsRemoved && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !threadInfos[threadInfo.parentThreadID])) ) { updateInfos.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }); } else { const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter( member => !removedUserIDsSet.has(member.id), ), timestamps: { ...threadInfo.timestamps, members: memberTimestamps, }, }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos, updateInfos, }; }, canBeProcessed: async ( dmOperation: DMRemoveMembersOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); export { removeMembersSpec }; diff --git a/lib/shared/dm-ops/send-edit-message-spec.js b/lib/shared/dm-ops/send-edit-message-spec.js index 79f9f67dd..dee0cf0d8 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,66 +1,70 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendEditMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendEditMessageOperation, ) { - const { threadID, creatorID, time, targetMessageID, text } = dmOperation; - return { + 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({ notificationsCreationData: async ( dmOperation: DMSendEditMessageOperation, ) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async (dmOperation: DMSendEditMessageOperation) => { - const { messageID } = dmOperation; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; return { rawMessageInfos, updateInfos: [], }; }, canBeProcessed: async ( dmOperation: DMSendEditMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const message = await utilities.fetchMessage(dmOperation.targetMessageID); if (!message) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, }); 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 25fbabc24..c580455d1 100644 --- a/lib/shared/dm-ops/send-multimedia-message-spec.js +++ b/lib/shared/dm-ops/send-multimedia-message-spec.js @@ -1,66 +1,68 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendMultimediaMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; -import type { MediaMessageData } from '../../types/messages/media.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendMultimediaMessageOperation, -): MediaMessageData { - const { threadID, creatorID, time, media } = dmOperation; - return { +) { + const { threadID, creatorID, time, media, messageID } = dmOperation; + const messageData = { type: messageTypes.MULTIMEDIA, threadID, creatorID, time, media, }; + const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); + return { messageData, rawMessageInfo }; } const sendMultimediaMessageSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMSendMultimediaMessageOperation, ) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async ( dmOperation: DMSendMultimediaMessageOperation, ) => { - const { messageID } = dmOperation; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; return { rawMessageInfos, updateInfos, }; }, canBeProcessed: async ( dmOperation: DMSendMultimediaMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: false, }); 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 c7b216545..b5bf897d1 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,68 +1,78 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendReactionMessageOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -function createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendReactionMessageOperation, ) { - const { threadID, creatorID, time, targetMessageID, reaction, action } = - dmOperation; - return { + 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({ notificationsCreationData: async ( dmOperation: DMSendReactionMessageOperation, ) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async (dmOperation: DMSendReactionMessageOperation) => { - const { messageID } = dmOperation; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; return { rawMessageInfos, updateInfos: [], }; }, canBeProcessed: async ( dmOperation: DMSendReactionMessageOperation, utilities: ProcessDMOperationUtilities, ) => { const message = await utilities.fetchMessage(dmOperation.targetMessageID); if (!message) { return { isProcessingPossible: false, reason: { type: 'missing_message', messageID: dmOperation.targetMessageID, }, }; } return { isProcessingPossible: true, }; }, supportsAutoRetry: true, }); 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 5b3e1dcce..6711201b2 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,63 +1,66 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMSendTextMessageOperation } 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 createMessageDataFromDMOperation( +function createMessageDataWithInfoFromDMOperation( dmOperation: DMSendTextMessageOperation, ) { - const { threadID, creatorID, time, text } = dmOperation; - return { + 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({ notificationsCreationData: async ( dmOperation: DMSendTextMessageOperation, ) => { - const messageData = createMessageDataFromDMOperation(dmOperation); - return { messageDatas: [messageData] }; + return { + messageDatasWithMessageInfos: [ + createMessageDataWithInfoFromDMOperation(dmOperation), + ], + }; }, processDMOperation: async (dmOperation: DMSendTextMessageOperation) => { - const { messageID } = dmOperation; - const messageData = createMessageDataFromDMOperation(dmOperation); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const { rawMessageInfo } = + createMessageDataWithInfoFromDMOperation(dmOperation); + const rawMessageInfos = [rawMessageInfo]; const updateInfos: Array = []; return { rawMessageInfos, updateInfos, }; }, canBeProcessed: async ( dmOperation: DMSendTextMessageOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: false, }); export { sendTextMessageSpec }; diff --git a/lib/shared/dm-ops/update-relationship-spec.js b/lib/shared/dm-ops/update-relationship-spec.js index d3d3acd23..a7c4787f8 100644 --- a/lib/shared/dm-ops/update-relationship-spec.js +++ b/lib/shared/dm-ops/update-relationship-spec.js @@ -1,100 +1,108 @@ // @flow import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMUpdateRelationshipOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; -async function createMessageDataFromDMOperation( +async function createMessageDataWithInfoFromDMOperation( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) { - const { threadID, creatorID, time, operation } = dmOperation; + const { threadID, creatorID, time, operation, messageID } = dmOperation; const { viewerID, findUserIdentities } = utilities; if (operation !== 'farcaster_mutual') { - return { + const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, targetID: viewerID, time, operation, }; + const rawMessageInfo = rawMessageInfoFromMessageData( + messageData, + messageID, + ); + return { rawMessageInfo, messageData }; } const { identities: userIdentities } = await findUserIdentities([ creatorID, viewerID, ]); const creatorFID = userIdentities[creatorID]?.farcasterID; const targetFID = userIdentities[viewerID]?.farcasterID; if (!creatorFID || !targetFID) { const errorMap = { creatorID: creatorFID, viewerID: targetFID }; throw new Error( 'could not fetch FID for either creator or target: ' + JSON.stringify(errorMap), ); } - return { + const messageData = { type: messageTypes.UPDATE_RELATIONSHIP, threadID, creatorID, creatorFID, targetID: viewerID, targetFID, time, operation, }; + const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); + return { rawMessageInfo, messageData }; } const updateRelationshipSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { - const messageData = await createMessageDataFromDMOperation( - dmOperation, - utilities, - ); - return { messageDatas: [messageData] }; + const messageDataWithMessageInfo = + await createMessageDataWithInfoFromDMOperation( + dmOperation, + + utilities, + ); + return { + messageDatasWithMessageInfos: [messageDataWithMessageInfo], + }; }, processDMOperation: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { - const { messageID } = dmOperation; - const messageData = await createMessageDataFromDMOperation( + const { rawMessageInfo } = await createMessageDataWithInfoFromDMOperation( dmOperation, utilities, ); - const rawMessageInfos = [ - rawMessageInfoFromMessageData(messageData, messageID), - ]; + const rawMessageInfos = [rawMessageInfo]; return { rawMessageInfos, updateInfos: [], }; }, canBeProcessed: async ( dmOperation: DMUpdateRelationshipOperation, utilities: ProcessDMOperationUtilities, ) => { try { - await createMessageDataFromDMOperation(dmOperation, utilities); + await createMessageDataWithInfoFromDMOperation(dmOperation, utilities); return { isProcessingPossible: true, }; } catch (e) { return { isProcessingPossible: false, reason: { type: 'invalid' }, }; } }, supportsAutoRetry: true, }); export { updateRelationshipSpec }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index 56e8e50d5..1be6611f7 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,420 +1,423 @@ // @flow import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; -import type { MessageData } from './message-types.js'; +import type { MessageData, RawMessageInfo } from './message-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; import { tShape } from '../utils/validation-utils.js'; export type NotifTexts = { +merged: string | EntityText, +body: string | EntityText, +title: string | ThreadEntity, +prefix?: string | EntityText, }; export type ResolvedNotifTexts = { +merged: string, +body: string, +title: string, +prefix?: string, }; export const resolvedNotifTextsValidator: TInterface = tShape({ merged: t.String, body: t.String, title: t.String, prefix: t.maybe(t.String), }); export type NotificationsCreationData = | { - +messageDatas: $ReadOnlyArray, + +messageDatasWithMessageInfos: ?$ReadOnlyArray<{ + +messageData: MessageData, + +rawMessageInfo: RawMessageInfo, + }>, } | { +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, };