diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js index 44c1f697b..e7ccb5d1a 100644 --- a/lib/shared/dm-ops/add-members-spec.js +++ b/lib/shared/dm-ops/add-members-spec.js @@ -1,178 +1,199 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { createRoleAndPermissionForThickThreads } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMAddMembersOperation, dmAddMembersOperationValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; import { minimallyEncodeMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; +import type { ThreadPermissionsInfo } from '../../types/thread-permission-types.js'; import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { roleIsDefaultRole, userIsMember } from '../thread-utils.js'; function createAddNewMembersMessageDataWithInfoFromDMOperation( dmOperation: DMAddMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, threadID, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } +function createPermissionsForNewMembers( + threadInfo: ThickRawThreadInfo, + utilities: ProcessDMOperationUtilities, +): { + +membershipPermissions: ThreadPermissionsInfo, + +roleID: string, +} { + const defaultRoleID = values(threadInfo.roles).find(role => + roleIsDefaultRole(role), + )?.id; + invariant(defaultRoleID, 'Default role ID must exist'); + + const { parentThreadID } = threadInfo; + const parentThreadInfo = parentThreadID + ? utilities.threadInfos[parentThreadID] + : null; + if (parentThreadID && !parentThreadInfo) { + console.log( + `Parent thread with ID ${parentThreadID} was expected while adding ` + + 'thread members but is missing from the store', + ); + } + invariant( + !parentThreadInfo || parentThreadInfo.thick, + 'Parent thread should be thick', + ); + + const { membershipPermissions } = createRoleAndPermissionForThickThreads( + threadInfo.type, + threadInfo.id, + defaultRoleID, + parentThreadInfo, + ); + + return { + membershipPermissions, + roleID: defaultRoleID, + }; +} + const addMembersSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async (dmOperation: DMAddMembersOperation) => { return { messageDatasWithMessageInfos: [ createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation), ], }; }, processDMOperation: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { editorID, time, addedUserIDs, threadID } = dmOperation; const { viewerID, threadInfos } = utilities; const { rawMessageInfo } = createAddNewMembersMessageDataWithInfoFromDMOperation(dmOperation); const rawMessageInfos = [rawMessageInfo]; const currentThreadInfo = threadInfos[threadID]; if (!currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], blobOps: [], }; } - const defaultRoleID = values(currentThreadInfo.roles).find(role => - roleIsDefaultRole(role), - )?.id; - invariant(defaultRoleID, 'Default role ID must exist'); - - const parentThreadID = currentThreadInfo.parentThreadID; - const parentThreadInfo = parentThreadID - ? utilities.threadInfos[parentThreadID] - : null; - if (parentThreadID && !parentThreadInfo) { - console.log( - `Parent thread with ID ${parentThreadID} was expected while adding ` + - 'thread members but is missing from the store', - ); - } - invariant( - !parentThreadInfo || parentThreadInfo.thick, - 'Parent thread should be thick', - ); - - const { membershipPermissions } = createRoleAndPermissionForThickThreads( - currentThreadInfo.type, - currentThreadInfo.id, - defaultRoleID, - parentThreadInfo, + const { membershipPermissions, roleID } = createPermissionsForNewMembers( + currentThreadInfo, + utilities, ); const memberTimestamps = { ...currentThreadInfo.timestamps.members }; const newMembers = []; for (const userID of addedUserIDs) { if (!memberTimestamps[userID]) { memberTimestamps[userID] = { isMember: time, subscription: currentThreadInfo.creationTime, }; } if (memberTimestamps[userID].isMember > time) { continue; } memberTimestamps[userID] = { ...memberTimestamps[userID], isMember: time, }; if (userIsMember(currentThreadInfo, userID)) { continue; } newMembers.push( minimallyEncodeMemberInfo({ id: userID, - role: defaultRoleID, + role: roleID, permissions: membershipPermissions, isSender: editorID === viewerID, subscription: joinThreadSubscription, }), ); } const resultThreadInfo = { ...currentThreadInfo, members: [...currentThreadInfo.members, ...newMembers], timestamps: { ...currentThreadInfo.timestamps, members: memberTimestamps, }, }; const updateInfos = [ { type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, }, ]; return { rawMessageInfos, updateInfos, blobOps: [], }; }, canBeProcessed: async ( dmOperation: DMAddMembersOperation, utilities: ProcessDMOperationUtilities, ) => { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, operationValidator: dmAddMembersOperationValidator, }); export { addMembersSpec, createAddNewMembersMessageDataWithInfoFromDMOperation, + createPermissionsForNewMembers, }; diff --git a/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js b/lib/shared/dm-ops/add-viewer-to-thread-members-spec.js index 83812a9b4..cd032b23b 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,169 +1,223 @@ // @flow import uuid from 'uuid'; +import { createPermissionsForNewMembers } from './add-members-spec.js'; import { createThickRawThreadInfo } from './create-thread-spec.js'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import { type DMAddViewerToThreadMembersOperation, dmAddViewerToThreadMembersValidator, } from '../../types/dm-ops.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { RawMessageInfo } from '../../types/message-types.js'; import { messageTruncationStatus } from '../../types/message-types.js'; import type { AddMembersMessageData } from '../../types/messages/add-members.js'; +import { + minimallyEncodeMemberInfo, + minimallyEncodeThreadCurrentUserInfo, +} from '../../types/minimally-encoded-thread-permissions-types.js'; import { joinThreadSubscription } from '../../types/subscription-types.js'; +import type { ThickMemberInfo } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; import { userIsMember } from '../thread-utils.js'; function createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( dmOperation: DMAddViewerToThreadMembersOperation, ): { +messageData: AddMembersMessageData, +rawMessageInfo: RawMessageInfo, } { const { editorID, time, addedUserIDs, existingThreadDetails, messageID } = dmOperation; const messageData = { type: messageTypes.ADD_MEMBERS, threadID: existingThreadDetails.threadID, creatorID: editorID, time, addedUserIDs: [...addedUserIDs], }; const rawMessageInfo = rawMessageInfoFromMessageData(messageData, messageID); return { messageData, rawMessageInfo }; } const addViewerToThreadMembersSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMAddViewerToThreadMembersOperation, ) => { return { messageDatasWithMessageInfos: [ createAddViewerToThreadMembersMessageDataWithInfoFromDMOp( dmOperation, ), ], }; }, processDMOperation: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { - const { time, messageID, addedUserIDs, existingThreadDetails } = + const { time, messageID, addedUserIDs, existingThreadDetails, editorID } = dmOperation; const { threadInfos } = utilities; const { rawMessageInfo } = createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation); const rawMessageInfos = messageID ? [rawMessageInfo] : []; const threadID = existingThreadDetails.threadID; const currentThreadInfo = threadInfos[threadID]; if (currentThreadInfo && !currentThreadInfo.thick) { return { rawMessageInfos: [], updateInfos: [], blobOps: [], }; } 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); } } + if (currentThreadInfo) { + const { membershipPermissions, roleID } = + createPermissionsForNewMembers(currentThreadInfo, utilities); + + const newMemberInfos = newMembers.map(userID => + minimallyEncodeMemberInfo({ + id: userID, + role: roleID, + permissions: membershipPermissions, + isSender: editorID === utilities.viewerID, + subscription: joinThreadSubscription, + }), + ); + + const resultThreadInfo = { + ...currentThreadInfo, + members: [...currentThreadInfo.members, ...newMemberInfos], + currentUser: minimallyEncodeThreadCurrentUserInfo({ + role: roleID, + permissions: membershipPermissions, + subscription: joinThreadSubscription, + unread: true, + }), + timestamps: { + ...currentThreadInfo.timestamps, + members: { + ...currentThreadInfo.timestamps.members, + ...memberTimestamps, + }, + }, + }; + + const updateInfos = [ + { + type: updateTypes.UPDATE_THREAD, + id: uuid.v4(), + time, + threadInfo: resultThreadInfo, + }, + ]; + + return { + rawMessageInfos, + updateInfos, + blobOps: [], + }; + } + const resultThreadInfo = createThickRawThreadInfo( { ...existingThreadDetails, allMemberIDsWithSubscriptions: [ ...existingThreadDetails.allMemberIDsWithSubscriptions, ...newMembers.map(id => ({ id, subscription: joinThreadSubscription, })), ], timestamps: { ...existingThreadDetails.timestamps, members: { ...existingThreadDetails.timestamps.members, ...memberTimestamps, }, }, }, utilities, ); const updateInfos = [ { type: updateTypes.JOIN_THREAD, id: uuid.v4(), time, threadInfo: resultThreadInfo, rawMessageInfos, truncationStatus: messageTruncationStatus.EXHAUSTIVE, rawEntryInfos: [], }, ]; return { - rawMessageInfos, + rawMessageInfos: [], updateInfos, blobOps: [], }; }, canBeProcessed: async ( dmOperation: DMAddViewerToThreadMembersOperation, utilities: ProcessDMOperationUtilities, ) => { const { viewerID } = utilities; // We expect the viewer to be in the added users when the DM op // is processed. An exception is for ops generated // by InitialStateSharingHandler, which won't contain a messageID if ( dmOperation.addedUserIDs.includes(viewerID) || !dmOperation.messageID ) { return { isProcessingPossible: true }; } console.log('Invalid DM operation', dmOperation); return { isProcessingPossible: false, reason: { type: 'invalid', }, }; }, supportsAutoRetry: true, operationValidator: dmAddViewerToThreadMembersValidator, }); export { addViewerToThreadMembersSpec, createAddViewerToThreadMembersMessageDataWithInfoFromDMOp, };