diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js index 6830805d9..295710301 100644 --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,45 +1,43 @@ // @flow import { addMembersSpec } from './add-members-spec.js'; import { addViewerToThreadMembersSpec } from './add-viewer-to-thread-members-spec.js'; import { changeThreadReadStatusSpec } from './change-thread-read-status-spec.js'; import { changeThreadSettingsSpec } from './change-thread-settings-spec.js'; import { changeThreadSubscriptionSpec } from './change-thread-subscription.js'; import { createEntrySpec } from './create-entry-spec.js'; import { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; import { deleteEntrySpec } from './delete-entry-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; import { editEntrySpec } from './edit-entry-spec.js'; import { joinThreadSpec } from './join-thread-spec.js'; import { leaveThreadSpec } from './leave-thread-spec.js'; -import { removeMembersSpec } from './remove-members-spec.js'; import { sendEditMessageSpec } from './send-edit-message-spec.js'; import { sendMultimediaMessageSpec } from './send-multimedia-message-spec.js'; import { sendReactionMessageSpec } from './send-reaction-message-spec.js'; import { sendTextMessageSpec } from './send-text-message-spec.js'; import { updateRelationshipSpec } from './update-relationship-spec.js'; import { type DMOperationType, dmOperationTypes } from '../../types/dm-ops.js'; export const dmOpSpecs: { +[DMOperationType]: DMOperationSpec, } = Object.freeze({ [dmOperationTypes.CREATE_THREAD]: createThreadSpec, [dmOperationTypes.CREATE_SIDEBAR]: createSidebarSpec, [dmOperationTypes.SEND_TEXT_MESSAGE]: sendTextMessageSpec, [dmOperationTypes.SEND_MULTIMEDIA_MESSAGE]: sendMultimediaMessageSpec, [dmOperationTypes.SEND_REACTION_MESSAGE]: sendReactionMessageSpec, [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec, [dmOperationTypes.ADD_MEMBERS]: addMembersSpec, [dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS]: addViewerToThreadMembersSpec, [dmOperationTypes.JOIN_THREAD]: joinThreadSpec, [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, - [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec, [dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION]: changeThreadSubscriptionSpec, [dmOperationTypes.CHANGE_THREAD_READ_STATUS]: changeThreadReadStatusSpec, [dmOperationTypes.CREATE_ENTRY]: createEntrySpec, [dmOperationTypes.DELETE_ENTRY]: deleteEntrySpec, [dmOperationTypes.EDIT_ENTRY]: editEntrySpec, [dmOperationTypes.UPDATE_RELATIONSHIP]: updateRelationshipSpec, }); diff --git a/lib/shared/dm-ops/remove-members-spec.js b/lib/shared/dm-ops/remove-members-spec.js deleted file mode 100644 index 064c2f357..000000000 --- a/lib/shared/dm-ops/remove-members-spec.js +++ /dev/null @@ -1,134 +0,0 @@ -// @flow - -import uuid from 'uuid'; - -import type { - DMOperationSpec, - ProcessDMOperationUtilities, -} from './dm-op-spec.js'; -import { - type DMRemoveMembersOperation, - dmRemoveMembersOperationValidator, -} 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 createMessageDataWithInfoFromDMOperation( - dmOperation: DMRemoveMembersOperation, -) { - 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({ - processDMOperation: async ( - dmOperation: DMRemoveMembersOperation, - utilities: ProcessDMOperationUtilities, - ) => { - const { time, threadID, removedUserIDs } = dmOperation; - const { viewerID, threadInfos } = utilities; - const threadInfo = threadInfos[threadID]; - - const messageDataWithMessageInfos = - createMessageDataWithInfoFromDMOperation(dmOperation); - const { rawMessageInfo } = messageDataWithMessageInfos; - 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, - }); - } - - const notificationsCreationData = { - messageDatasWithMessageInfos: [messageDataWithMessageInfos], - }; - - return { - rawMessageInfos, - updateInfos, - blobOps: [], - notificationsCreationData, - }; - }, - 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, - operationValidator: dmRemoveMembersOperationValidator, - }); - -export { removeMembersSpec }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 7182a125a..72bc169c9 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,611 +1,590 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { BlobOperation } from './holder-types.js'; import { type Media, mediaValidator } from './media-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { RelationshipOperation } from './messages/update-relationship.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import { threadTimestampsValidator, type ThreadTimestamps, } from './thread-types.js'; import type { ClientUpdateInfo } from './update-types.js'; import { values } from '../utils/objects.js'; import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js'; export const dmOperationTypes = Object.freeze({ CREATE_THREAD: 'create_thread', CREATE_SIDEBAR: 'create_sidebar', SEND_TEXT_MESSAGE: 'send_text_message', SEND_MULTIMEDIA_MESSAGE: 'send_multimedia_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', ADD_MEMBERS: 'add_members', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', - REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SUBSCRIPTION: 'change_thread_subscription', CHANGE_THREAD_READ_STATUS: 'change_thread_read_status', CREATE_ENTRY: 'create_entry', DELETE_ENTRY: 'delete_entry', EDIT_ENTRY: 'edit_entry', UPDATE_RELATIONSHIP: 'update_relationship', }); export type DMOperationType = $Values; type MemberIDWithSubscription = { +id: string, +subscription: ThreadSubscription, }; export const memberIDWithSubscriptionValidator: TInterface = tShape({ id: tUserID, subscription: threadSubscriptionValidator, }); export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDsWithSubscriptions: $ReadOnlyArray, +roleID: string, +unread: boolean, +timestamps: ThreadTimestamps, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ threadID: t.String, threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), allMemberIDsWithSubscriptions: t.list(memberIDWithSubscriptionValidator), roleID: t.String, unread: t.Boolean, timestamps: threadTimestampsValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: t.maybe(t.String), sourceMessageID: t.maybe(t.String), repliesCount: t.maybe(t.Number), pinnedCount: t.maybe(t.Number), }); export type DMCreateThreadOperation = { +type: 'create_thread', +threadID: string, +creatorID: string, +time: number, +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, +roleID: string, +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_THREAD), threadID: t.String, creatorID: tUserID, time: t.Number, threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), roleID: t.String, newMessageID: t.String, }); export type DMCreateSidebarOperation = { +type: 'create_sidebar', +threadID: string, +creatorID: string, +time: number, +parentThreadID: string, +memberIDs: $ReadOnlyArray, +sourceMessageID: string, +roleID: string, +newSidebarSourceMessageID: string, +newCreateSidebarMessageID: string, }; export const dmCreateSidebarOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_SIDEBAR), threadID: t.String, creatorID: tUserID, time: t.Number, parentThreadID: t.String, memberIDs: t.list(tUserID), sourceMessageID: t.String, roleID: t.String, newSidebarSourceMessageID: t.String, newCreateSidebarMessageID: t.String, }); export type DMSendTextMessageOperation = { +type: 'send_text_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +text: string, }; export const dmSendTextMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_TEXT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, text: t.String, }); export type DMSendMultimediaMessageOperation = { +type: 'send_multimedia_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +media: $ReadOnlyArray, }; export const dmSendMultimediaMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_MULTIMEDIA_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, media: t.list(mediaValidator), }); export type DMSendReactionMessageOperation = { +type: 'send_reaction_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export const dmSendReactionMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_REACTION_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, reaction: t.String, action: t.enums.of(['add_reaction', 'remove_reaction']), }); export type DMSendEditMessageOperation = { +type: 'send_edit_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +text: string, }; export const dmSendEditMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_EDIT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, text: t.String, }); type DMAddMembersBase = { +editorID: string, +time: number, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, +messageID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, messageID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +messageID: ?string, +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), messageID: t.maybe(t.String), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: tUserID, time: t.Number, messageID: t.String, existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMLeaveThreadOperation = { +type: 'leave_thread', +editorID: string, +time: number, +messageID: string, +threadID: string, }; export const dmLeaveThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.LEAVE_THREAD), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, }); -export type DMRemoveMembersOperation = { - +type: 'remove_members', - +editorID: string, - +time: number, - +messageID: string, - +threadID: string, - +removedUserIDs: $ReadOnlyArray, -}; -export const dmRemoveMembersOperationValidator: TInterface = - tShape({ - type: tString(dmOperationTypes.REMOVE_MEMBERS), - editorID: tUserID, - time: t.Number, - messageID: t.String, - threadID: t.String, - removedUserIDs: t.list(tUserID), - }); - export type DMThreadSettingsChanges = { +name?: string, +description?: string, +color?: string, +avatar?: ClientAvatar | null, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), threadID: t.String, editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, }); export type DMChangeThreadSubscriptionOperation = { +type: 'change_thread_subscription', +time: number, +threadID: string, +creatorID: string, +subscription: ThreadSubscription, }; export const dmChangeThreadSubscriptionOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION), time: t.Number, threadID: t.String, creatorID: tUserID, subscription: threadSubscriptionValidator, }); export type DMChangeThreadReadStatusOperation = { +type: 'change_thread_read_status', +time: number, +threadID: string, +creatorID: string, +unread: boolean, }; export const dmChangeThreadReadStatusOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_READ_STATUS), time: t.Number, threadID: t.String, creatorID: tUserID, unread: t.Boolean, }); export type ComposableDMOperation = | DMSendTextMessageOperation | DMSendMultimediaMessageOperation; export type DMCreateEntryOperation = { +type: 'create_entry', +threadID: string, +creatorID: string, +time: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmCreateEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_ENTRY), threadID: t.String, creatorID: tUserID, time: t.Number, entryID: t.String, entryDate: t.String, text: t.String, messageID: t.String, }); export type DMDeleteEntryOperation = { +type: 'delete_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +prevText: string, +messageID: string, }; export const dmDeleteEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.DELETE_ENTRY), threadID: t.String, creatorID: tUserID, time: t.Number, creationTime: t.Number, entryID: t.String, entryDate: t.String, prevText: t.String, messageID: t.String, }); export type DMEditEntryOperation = { +type: 'edit_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmEditEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.EDIT_ENTRY), threadID: t.String, creatorID: tUserID, creationTime: t.Number, time: t.Number, entryID: t.String, entryDate: t.String, text: t.String, messageID: t.String, }); export type DMUpdateRelationshipOperation = { +type: 'update_relationship', +threadID: string, +creatorID: string, +time: number, +operation: RelationshipOperation, +targetUserID: string, +messageID: string, }; export const dmUpdateRelationshipOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.UPDATE_RELATIONSHIP), threadID: t.String, creatorID: tUserID, time: t.Number, operation: t.enums.of([ 'request_sent', 'request_accepted', 'farcaster_mutual', ]), targetUserID: tUserID, messageID: t.String, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendMultimediaMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation - | DMRemoveMembersOperation | DMChangeThreadSettingsOperation | DMChangeThreadSubscriptionOperation | DMChangeThreadReadStatusOperation | DMCreateEntryOperation | DMDeleteEntryOperation | DMEditEntryOperation | DMUpdateRelationshipOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendMultimediaMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, - dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSubscriptionOperationValidator, dmChangeThreadReadStatusOperationValidator, dmCreateEntryOperationValidator, dmDeleteEntryOperationValidator, dmEditEntryOperationValidator, dmUpdateRelationshipOperationValidator, ]); export type DMBlobOperation = $ReadOnly<{ ...BlobOperation, +dmOpType: 'inbound_only' | 'outbound_only' | 'inbound_and_outbound', }>; export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, blobOps: Array, notificationsCreationData: ?NotificationsCreationData, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +composableMessageID: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +timestamp: number, +condition: | { +type: 'thread', +threadID: string, } | { +type: 'entry', +entryID: string, } | { +type: 'message', +messageID: string, } | { +type: 'membership', +threadID: string, +userID: string, }, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export const clearQueuedMessageDMOpsActionType = 'CLEAR_QUEUED_MESSAGE_DM_OPS'; export type ClearQueuedMessageDMOpsPayload = { +messageID: string, }; export const clearQueuedEntryDMOpsActionType = 'CLEAR_QUEUED_ENTRY_DM_OPS'; export type ClearQueuedEntryDMOpsPayload = { +entryID: string, }; export const clearQueuedMembershipDMOpsActionType = 'CLEAR_QUEUED_MEMBERSHIP_DM_OPS'; export type ClearQueuedMembershipDMOpsPayload = { +threadID: string, +userID: string, }; export type OperationsQueue = $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>; export type QueuedDMOperations = { +threadQueue: { +[threadID: string]: OperationsQueue, }, +messageQueue: { +[messageID: string]: OperationsQueue, }, +entryQueue: { +[entryID: string]: OperationsQueue, }, +membershipQueue: { +[threadID: string]: { +[memberID: string]: OperationsQueue, }, }, };