diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index 7d45c0d3f..fe37b7822 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,126 +1,127 @@ // @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 { minimallyEncodeMemberInfo, type ThickRawThreadInfo, } 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 { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; export type AddMembersResult = { rawMessageInfos: Array, updateInfos: Array, threadInfo: ?ThickRawThreadInfo, }; function createAddNewMembersResults( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ): AddMembersResult { const { editorID, time, messageID, addedUserIDs, threadID } = dmOperation; const addMembersMessage = { type: messageTypes.ADD_MEMBERS, id: messageID, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const currentThreadInfo = utilities.threadInfos[threadID]; if (!currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], threadInfo: null, }; } 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 newMembers = addedUserIDs .filter(userID => !userIsMember(currentThreadInfo, userID)) .map(userID => minimallyEncodeMemberInfo({ id: userID, role: defaultRoleID, permissions: membershipPermissions, isSender: editorID === viewerID, subscription: joinThreadSubscription, }), ); const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMembers], }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; return { rawMessageInfos: [addMembersMessage], updateInfos, threadInfo: resultThreadInfo, }; } const addMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { rawMessageInfos, updateInfos } = createAddNewMembersResults( dmOperation, viewerID, utilities, ); return { rawMessageInfos, updateInfos }; }, canBeProcessed( dmOperation: DMAddMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, + supportsAutoRetry: true, }); export { addMembersSpec, createAddNewMembersResults }; 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 4ace51e88..b5fcc6124 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,80 +1,81 @@ // @flow import uuid from 'uuid'; import type { AddMembersResult } from './add-members-spec.js'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; import type { DMAddViewerToThreadMembersOperation } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; function createAddViewerToThreadMembersResults( dmOperation: DMAddViewerToThreadMembersOperation, viewerID: string, ): AddMembersResult { const { editorID, time, messageID, addedUserIDs, existingThreadDetails } = dmOperation; const addMembersMessage = { type: messageTypes.ADD_MEMBERS, id: messageID, threadID: existingThreadDetails.threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs], }, viewerID, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, rawMessageInfos: [addMembersMessage], truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; return { rawMessageInfos: [], updateInfos, threadInfo: resultThreadInfo, }; } const addViewerToThreadMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMAddViewerToThreadMembersOperation, viewerID: string, ) => { const { rawMessageInfos, updateInfos } = createAddViewerToThreadMembersResults(dmOperation, viewerID); return { rawMessageInfos, updateInfos }; }, canBeProcessed( dmOperation: DMAddViewerToThreadMembersOperation, viewerID: string, ) { if (dmOperation.addedUserIDs.includes(viewerID)) { return { isProcessingPossible: true }; } console.log('Invalid DM operation', dmOperation); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; }, + supportsAutoRetry: true, }); export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults }; diff --git a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js index 9c1dfa11a..28c5a1dc9 100644 --- a/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-and-add-viewer-spec.js @@ -1,77 +1,78 @@ // @flow import { addViewerToThreadMembersSpec, createAddViewerToThreadMembersResults, } from './add-viewer-to-thread-members-spec.js'; import { processChangeSettingsOperation } from './change-thread-settings-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsAndAddViewerOperation } from '../../types/dm-ops.js'; function createAddViewerAndMembersOperation( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, ) { const { editorID, time, messageIDsPrefix, changes, existingThreadDetails } = dmOperation; const newMemberIDs = changes.newMemberIDs && changes.newMemberIDs.length > 0 ? [...new Set(changes.newMemberIDs)] : []; return { type: 'add_viewer_to_thread_members', editorID, time, messageID: `${messageIDsPrefix}/add_members`, addedUserIDs: newMemberIDs, existingThreadDetails, }; } function processAddViewerToThreadMembersOperation( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, ) { const operation = createAddViewerAndMembersOperation(dmOperation); if (operation.addedUserIDs.length === 0) { return null; } return createAddViewerToThreadMembersResults(operation, viewerID); } const changeThreadSettingsAndAddViewerSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const addMembersResult = processAddViewerToThreadMembersOperation( dmOperation, viewerID, ); return processChangeSettingsOperation( dmOperation, viewerID, utilities, addMembersResult, ); }, canBeProcessed( dmOperation: DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { const operation = createAddViewerAndMembersOperation(dmOperation); return addViewerToThreadMembersSpec.canBeProcessed( operation, viewerID, utilities, ); }, + supportsAutoRetry: true, }); export { changeThreadSettingsAndAddViewerSpec }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 4b251b4ab..514a4941b 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,181 +1,182 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { type AddMembersResult, createAddNewMembersResults, } from './add-members-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsAndAddViewerOperation, DMChangeThreadSettingsOperation, DMOperationResult, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { LegacyRawThreadInfo } 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'; function processAddMembersOperation( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { const { editorID, time, messageIDsPrefix, changes, threadID } = dmOperation; const newMemberIDs = changes.newMemberIDs && changes.newMemberIDs.length > 0 ? [...new Set(changes.newMemberIDs)] : []; if (!changes.newMemberIDs || changes.newMemberIDs.length === 0) { return null; } const operation = { type: 'add_members', editorID, time, messageID: `${messageIDsPrefix}/add_members`, addedUserIDs: newMemberIDs, threadID, }; return createAddNewMembersResults(operation, viewerID, utilities); } function processChangeSettingsOperation( dmOperation: | DMChangeThreadSettingsOperation | DMChangeThreadSettingsAndAddViewerOperation, viewerID: string, utilities: ProcessDMOperationUtilities, addMembersResult: ?AddMembersResult, ): DMOperationResult { const { editorID, time, changes, messageIDsPrefix } = dmOperation; const { name, description, color, avatar } = changes; const threadID = dmOperation.type === 'change_thread_settings' ? dmOperation.threadID : dmOperation.existingThreadDetails.threadID; let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) = utilities.threadInfos[threadID]; const updateInfos: Array = []; const rawMessageInfos: Array = []; if (addMembersResult) { if (addMembersResult.threadInfo) { threadInfoToUpdate = addMembersResult.threadInfo; } updateInfos.push(...addMembersResult.updateInfos); rawMessageInfos.push(...addMembersResult.rawMessageInfos); } invariant(threadInfoToUpdate?.thick, 'Thread should be thick'); const changedFields: { [string]: string | number } = {}; if (name !== undefined && name !== null) { changedFields.name = name; threadInfoToUpdate = { ...threadInfoToUpdate, name, }; } if (description !== undefined && description !== null) { changedFields.description = description; threadInfoToUpdate = { ...threadInfoToUpdate, description, }; } if (color) { changedFields.color = color; threadInfoToUpdate = { ...threadInfoToUpdate, color, }; } if (avatar) { changedFields.avatar = JSON.stringify(avatar); threadInfoToUpdate = { ...threadInfoToUpdate, avatar, }; } for (const fieldName in changedFields) { const newValue = changedFields[fieldName]; rawMessageInfos.push({ type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: newValue, id: `${messageIDsPrefix}/${fieldName}`, }); } if (values(changedFields).length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } return { rawMessageInfos, updateInfos, }; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const addMembersResult = processAddMembersOperation( dmOperation, viewerID, utilities, ); return processChangeSettingsOperation( dmOperation, viewerID, utilities, addMembersResult, ); }, canBeProcessed( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, + supportsAutoRetry: true, }); export { changeThreadSettingsSpec, processChangeSettingsOperation }; diff --git a/lib/shared/dm-ops/create-sidebar-spec.js b/lib/shared/dm-ops/create-sidebar-spec.js index 376be5587..26f3e04fc 100644 --- a/lib/shared/dm-ops/create-sidebar-spec.js +++ b/lib/shared/dm-ops/create-sidebar-spec.js @@ -1,112 +1,113 @@ // @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 { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { isInvalidSidebarSource } from '../message-utils.js'; const createSidebarSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateSidebarOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { threadID, creatorID, time, parentThreadID, memberIDs, sourceMessageID, roleID, newSidebarSourceMessageID, newCreateSidebarMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType: threadTypes.THICK_SIDEBAR, creationTime: time, parentThreadID, allMemberIDs, roleID, creatorID, sourceMessageID, containingThreadID: parentThreadID, }, viewerID, ); 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 rawMessageInfos: Array = [ { type: messageTypes.SIDEBAR_SOURCE, id: newSidebarSourceMessageID, threadID, creatorID, time, sourceMessage, }, { type: messageTypes.CREATE_SIDEBAR, id: newCreateSidebarMessageID, threadID, creatorID, time: time + 1, sourceMessageAuthorID: sourceMessage.creatorID, initialThreadState: { parentThreadID, color: rawThreadInfo.color, memberIDs: allMemberIDs, }, }, ]; 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() { 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 b1517921b..9f9c9f648 100644 --- a/lib/shared/dm-ops/create-thread-spec.js +++ b/lib/shared/dm-ops/create-thread-spec.js @@ -1,197 +1,198 @@ // @flow import uuid from 'uuid'; import type { DMOperationSpec } 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 { type RawMessageInfo, 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'; 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, allMemberIDs, roleID, creatorID, name, avatar, description, color, containingThreadID, sourceMessageID, repliesCount, pinnedCount, } = input; const threadColor = color ?? generatePendingThreadColor(allMemberIDs); const { membershipPermissions, role } = createRoleAndPermissionForThickThreads(threadType, threadID, roleID); const newThread: MutableThickRawThreadInfo = { thick: true, minimallyEncoded: true, id: threadID, type: threadType, color: threadColor, creationTime, parentThreadID, members: allMemberIDs.map(memberID => minimallyEncodeMemberInfo({ id: memberID, role: role.id, permissions: membershipPermissions, isSender: memberID === viewerID, subscription: joinThreadSubscription, }), ), roles: { [role.id]: role, }, currentUser: minimallyEncodeThreadCurrentUserInfo({ role: role.id, permissions: membershipPermissions, subscription: joinThreadSubscription, unread: creatorID !== viewerID, }), repliesCount: repliesCount ?? 0, name, avatar, description, containingThreadID, }; if (sourceMessageID) { newThread.sourceMessageID = sourceMessageID; } if (pinnedCount) { newThread.pinnedCount = pinnedCount; } return newThread; } const createThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMCreateThreadOperation, viewerID: string, ) => { const { threadID, creatorID, time, threadType, memberIDs, roleID, newMessageID, } = dmOperation; const allMemberIDs = [creatorID, ...memberIDs]; const rawThreadInfo = createThickRawThreadInfo( { threadID, threadType, creationTime: time, allMemberIDs, roleID, creatorID, }, viewerID, ); const rawMessageInfos: Array = [ { type: messageTypes.CREATE_THREAD, id: newMessageID, threadID, creatorID, time, initialThreadState: { type: threadType, color: rawThreadInfo.color, memberIDs: allMemberIDs, }, }, ]; 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() { return { isProcessingPossible: true }; }, + supportsAutoRetry: true, }); export { createThickRawThreadInfo, createThreadSpec, createRoleAndPermissionForThickThreads, }; diff --git a/lib/shared/dm-ops/dm-op-spec.js b/lib/shared/dm-ops/dm-op-spec.js index 7aee30d4e..2b8ae2aca 100644 --- a/lib/shared/dm-ops/dm-op-spec.js +++ b/lib/shared/dm-ops/dm-op-spec.js @@ -1,31 +1,32 @@ // @flow import type { DMOperation, DMOperationResult } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; export type ProcessDMOperationUtilities = { // Needed to fetch sidebar source messages +fetchMessage: (messageID: string) => Promise, +threadInfos: RawThreadInfos, }; export type DMOperationSpec = { +processDMOperation: ( dmOp: DMOp, viewerID: string, utilities: ProcessDMOperationUtilities, ) => Promise, +canBeProcessed: ( dmOp: DMOp, viewerID: string, utilities: ProcessDMOperationUtilities, ) => | { +isProcessingPossible: true } | { +isProcessingPossible: false, +reason: | { +type: 'missing_thread', +threadID: string } | { +type: 'invalid' }, }, + +supportsAutoRetry: boolean, }; diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js index 0a94a8370..d9a18510f 100644 --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -1,111 +1,106 @@ // @flow import uuid from 'uuid'; +import { dmOpSpecs } from './dm-op-specs.js'; import type { DMOperation } from '../../types/dm-ops.js'; import type { InboundActionMetadata } from '../../types/redux-types.js'; import { outboundP2PMessageStatuses, type OutboundP2PMessage, } from '../../types/sqlite-types.js'; import { type DMOperationP2PMessage, userActionsP2PMessageTypes, } from '../../types/tunnelbroker/user-actions-peer-to-peer-message-types.js'; import type { CurrentUserInfo } from '../../types/user-types.js'; import { getContentSigningKey } from '../../utils/crypto-utils.js'; function generateMessagesToPeers( message: DMOperation, peers: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, - supportsAutoRetry: boolean, ): $ReadOnlyArray { const opMessage: DMOperationP2PMessage = { type: userActionsP2PMessageTypes.DM_OPERATION, op: message, }; const plaintext = JSON.stringify(opMessage); const outboundP2PMessages = []; for (const peer of peers) { const messageToPeer: OutboundP2PMessage = { messageID: uuid.v4(), deviceID: peer.deviceID, userID: peer.userID, timestamp: new Date().getTime().toString(), plaintext, ciphertext: '', status: outboundP2PMessageStatuses.persisted, - supportsAutoRetry, + supportsAutoRetry: dmOpSpecs[message.type].supportsAutoRetry, }; outboundP2PMessages.push(messageToPeer); } return outboundP2PMessages; } export const dmOperationSpecificationTypes = Object.freeze({ OUTBOUND: 'OutboundDMOperationSpecification', INBOUND: 'InboundDMOperationSpecification', }); // The operation generated on the sending client, causes changes to // the state and broadcasting information to peers. export type OutboundDMOperationSpecification = { +type: 'OutboundDMOperationSpecification', +op: DMOperation, - +supportsAutoRetry: boolean, +recipients: | { +type: 'all_peer_devices' | 'self_devices' } | { +type: 'some_users', +userIDs: $ReadOnlyArray }, }; // The operation received from other peers, causes changes to // the state and after processing, sends confirmation to the sender. export type InboundDMOperationSpecification = { +type: 'InboundDMOperationSpecification', +op: DMOperation, +metadata: ?InboundActionMetadata, }; export type DMOperationSpecification = | OutboundDMOperationSpecification | InboundDMOperationSpecification; async function createMessagesToPeersFromDMOp( operation: OutboundDMOperationSpecification, allPeerUserIDAndDeviceIDs: $ReadOnlyArray<{ +userID: string, +deviceID: string, }>, currentUserInfo: ?CurrentUserInfo, ): Promise<$ReadOnlyArray> { if (!currentUserInfo?.id) { return []; } let peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs; if (operation.recipients.type === 'self_devices') { peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter( peer => peer.userID === currentUserInfo.id, ); } else if (operation.recipients.type === 'some_users') { const userIDs = new Set(operation.recipients.userIDs); peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => userIDs.has(peer.userID), ); } const thisDeviceID = await getContentSigningKey(); const targetPeers = peerUserIDAndDeviceIDs.filter( peer => peer.deviceID !== thisDeviceID, ); - return generateMessagesToPeers( - operation.op, - targetPeers, - operation.supportsAutoRetry, - ); + return generateMessagesToPeers(operation.op, targetPeers); } export { createMessagesToPeersFromDMOp }; diff --git a/lib/shared/dm-ops/join-thread-spec.js b/lib/shared/dm-ops/join-thread-spec.js index f1254913f..8d30fe327 100644 --- a/lib/shared/dm-ops/join-thread-spec.js +++ b/lib/shared/dm-ops/join-thread-spec.js @@ -1,128 +1,129 @@ // @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 { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; const joinThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMJoinThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { joinerID, time, messageID, existingThreadDetails } = dmOperation; const currentThreadInfo = utilities.threadInfos[existingThreadDetails.threadID]; const joinThreadMessage = { type: messageTypes.JOIN_THREAD, id: messageID, threadID: existingThreadDetails.threadID, creatorID: joinerID, time, }; if (userIsMember(currentThreadInfo, joinerID)) { return { rawMessageInfos: [joinThreadMessage], updateInfos: [], }; } const updateInfos: Array = []; const rawMessageInfos: Array = []; if (viewerID === joinerID) { const newThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDs: [...existingThreadDetails.allMemberIDs, joinerID], }, viewerID, ); updateInfos.push({ type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: newThreadInfo, rawMessageInfos: [joinThreadMessage], truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }); } else { invariant(currentThreadInfo.thick, 'Thread should be thick'); rawMessageInfos.push(joinThreadMessage); 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], }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos, updateInfos, }; }, canBeProcessed( dmOperation: DMJoinThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.existingThreadDetails.threadID]) { 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 23d4e0907..62928cd01 100644 --- a/lib/shared/dm-ops/leave-thread-spec.js +++ b/lib/shared/dm-ops/leave-thread-spec.js @@ -1,86 +1,87 @@ // @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 { threadTypes } from '../../types/thread-types-enum.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { userIsMember } from '../thread-utils.js'; const leaveThreadSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMLeaveThreadOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, threadID } = dmOperation; const threadInfo = utilities.threadInfos[threadID]; invariant(threadInfo.thick, 'Thread should be thick'); const leaveThreadMessage = { type: messageTypes.LEAVE_THREAD, id: messageID, threadID, creatorID: editorID, time, }; const updateInfos: Array = []; if ( viewerID === editorID && userIsMember(threadInfo, editorID) && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !utilities.threadInfos[threadInfo.parentThreadID])) ) { updateInfos.push({ type: updateTypes.DELETE_THREAD, id: uuid.v4(), time, threadID, }); } else { const updatedThreadInfo = { ...threadInfo, members: threadInfo.members.filter(member => member.id !== editorID), }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos: [leaveThreadMessage], updateInfos, }; }, canBeProcessed( dmOperation: DMLeaveThreadOperation, viewerID: string, 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 16e8430b8..d378d5723 100644 --- a/lib/shared/dm-ops/remove-members-spec.js +++ b/lib/shared/dm-ops/remove-members-spec.js @@ -1,90 +1,91 @@ // @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'; const removeMembersSpec: DMOperationSpec = Object.freeze({ processDMOperation: async ( dmOperation: DMRemoveMembersOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, messageID, threadID, removedUserIDs } = dmOperation; const threadInfo = utilities.threadInfos[threadID]; invariant(threadInfo.thick, 'Thread should be thick'); const removeMembersMessage = { type: messageTypes.REMOVE_MEMBERS, id: messageID, threadID, time, creatorID: editorID, removedUserIDs: [...removedUserIDs], }; const removedUserIDsSet = new Set(removedUserIDs); const viewerIsRemoved = removedUserIDsSet.has(viewerID); const updateInfos: Array = []; if ( viewerIsRemoved && (threadInfo.type !== threadTypes.THICK_SIDEBAR || (threadInfo.parentThreadID && !utilities.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), ), }; updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: updatedThreadInfo, }); } return { rawMessageInfos: [removeMembersMessage], updateInfos, }; }, canBeProcessed( dmOperation: DMRemoveMembersOperation, viewerID: string, 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 9f50f2316..244ab55de 100644 --- a/lib/shared/dm-ops/send-edit-message-spec.js +++ b/lib/shared/dm-ops/send-edit-message-spec.js @@ -1,48 +1,49 @@ // @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'; const sendEditMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async (dmOperation: DMSendEditMessageOperation) => { const { threadID, creatorID, time, messageID, targetMessageID, text } = dmOperation; const editMessage = { type: messageTypes.EDIT_MESSAGE, id: messageID, threadID, creatorID, time, targetMessageID, text, }; return { rawMessageInfos: [editMessage], updateInfos: [], }; }, canBeProcessed( dmOperation: DMSendEditMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, + supportsAutoRetry: true, }); export { sendEditMessageSpec }; diff --git a/lib/shared/dm-ops/send-reaction-message-spec.js b/lib/shared/dm-ops/send-reaction-message-spec.js index 608778d47..472fea509 100644 --- a/lib/shared/dm-ops/send-reaction-message-spec.js +++ b/lib/shared/dm-ops/send-reaction-message-spec.js @@ -1,56 +1,57 @@ // @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'; const sendReactionMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async (dmOperation: DMSendReactionMessageOperation) => { const { threadID, creatorID, time, messageID, targetMessageID, reaction, action, } = dmOperation; const reactionMessage = { type: messageTypes.REACTION, id: messageID, threadID, creatorID, time, targetMessageID, reaction, action, }; return { rawMessageInfos: [reactionMessage], updateInfos: [], }; }, canBeProcessed( dmOperation: DMSendReactionMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, + 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 e65e423e6..8baeabe89 100644 --- a/lib/shared/dm-ops/send-text-message-spec.js +++ b/lib/shared/dm-ops/send-text-message-spec.js @@ -1,47 +1,48 @@ // @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'; const sendTextMessageSpec: DMOperationSpec = Object.freeze({ processDMOperation: async (dmOperation: DMSendTextMessageOperation) => { const { threadID, creatorID, time, messageID, text } = dmOperation; const textMessage = { type: messageTypes.TEXT, id: messageID, threadID, creatorID, time, text, }; const updateInfos: Array = []; return { rawMessageInfos: [textMessage], updateInfos, }; }, canBeProcessed( dmOperation: DMSendTextMessageOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, + supportsAutoRetry: false, }); export { sendTextMessageSpec };