diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -27,8 +27,7 @@ type RoleDeletionRequest, type RoleDeletionResult, } from 'lib/types/thread-types.js'; -import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; -import { values } from 'lib/utils/objects.js'; +import { threadSettingsChangesValidator } from 'lib/types/validators/thread-validators.js'; import { tShape, tNumEnum, @@ -118,15 +117,7 @@ export const updateThreadRequestInputValidator: TInterface = tShape({ threadID: tID, - changes: tShape({ - type: t.maybe(tNumEnum(values(threadTypes))), - name: t.maybe(t.String), - description: t.maybe(t.String), - color: t.maybe(tColor), - parentThreadID: t.maybe(tID), - newMemberIDs: t.maybe(t.list(tUserID)), - avatar: t.maybe(updateUserAvatarRequestValidator), - }), + changes: threadSettingsChangesValidator, accountPassword: t.maybe(tPassword), }); diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -0,0 +1,190 @@ +// @flow + +import uuid from 'uuid'; + +import { addMembersSpec } from './add-members-spec.js'; +import type { + DMOperationSpec, + ProcessDMOperationUtilities, +} from './dm-op-spec.js'; +import type { DMChangeThreadSettingsOperation } 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'; +import { firstLine } from '../../utils/string-utils.js'; +import { validChatNameRegex } from '../../utils/validation-utils.js'; + +const changeThreadSettingsSpec: DMOperationSpec = + Object.freeze({ + processDMOperation: async ( + dmOperation: DMChangeThreadSettingsOperation, + viewerID: string, + utilities: ProcessDMOperationUtilities, + ) => { + const { + editorID, + time, + changes, + messageIDsPrefix, + threadInfo, + rawMessageInfos, + truncationStatus, + rawEntryInfos, + } = dmOperation; + const { + name: untrimmedName, + description, + color, + parentThreadID, + avatar, + type: threadType, + } = changes; + const threadID = threadInfo.id; + + const newMemberIDs = + changes.newMemberIDs && changes.newMemberIDs.length > 0 + ? [...new Set(changes.newMemberIDs)] + : null; + + let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) = + utilities.getThreadInfo(threadID); + if (!threadInfoToUpdate && !newMemberIDs?.includes(viewerID)) { + // We can't perform this operation now. It should be queued for later. + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + + const updateInfos: Array = []; + const generatedRawMessageInfos: Array = []; + + if (newMemberIDs) { + const addMembersResult = await addMembersSpec.processDMOperation( + { + type: 'add_members', + editorID, + time, + messageID: `${messageIDsPrefix}/add_members`, + addedUserIDs: newMemberIDs, + threadInfo, + rawMessageInfos, + truncationStatus, + rawEntryInfos, + }, + viewerID, + utilities, + ); + const threadInfoFromUpdates = addMembersResult.updateInfos.find( + update => + update.type === updateTypes.UPDATE_THREAD || + update.type === updateTypes.JOIN_THREAD, + )?.threadInfo; + if (threadInfoFromUpdates) { + threadInfoToUpdate = threadInfoFromUpdates; + } + updateInfos.push(...addMembersResult.updateInfos); + generatedRawMessageInfos.push(...addMembersResult.rawMessageInfos); + } + + if (!threadInfoToUpdate || !threadInfoToUpdate.thick) { + // We can't perform this operation now. It should be queued for later. + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + + const changedFields: { [string]: string | number } = {}; + + if (untrimmedName !== undefined && untrimmedName !== null) { + const name = firstLine(untrimmedName); + if (name.search(validChatNameRegex) === -1) { + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + changedFields.name = name; + threadInfoToUpdate = { + ...threadInfoToUpdate, + name, + }; + } + + if (description !== undefined && description !== null) { + changedFields.description = description; + threadInfoToUpdate = { + ...threadInfoToUpdate, + description, + }; + } + + if (color) { + const newColor = color.toLowerCase(); + changedFields.color = newColor; + threadInfoToUpdate = { + ...threadInfoToUpdate, + color: newColor, + }; + } + + if (parentThreadID !== undefined) { + // TODO do we want to support this for thick threads? + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + + if (avatar) { + changedFields.avatar = + avatar.type !== 'remove' ? JSON.stringify(avatar) : ''; + threadInfoToUpdate = { + ...threadInfoToUpdate, + avatar: threadInfo.avatar, + }; + } + + if (threadType !== null && threadType !== undefined) { + // TODO do we want to support this for thick threads? + return { + rawMessageInfos: [], + updateInfos: [], + }; + } + + for (const fieldName in changedFields) { + const newValue = changedFields[fieldName]; + generatedRawMessageInfos.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: generatedRawMessageInfos, + updateInfos, + }; + }, + }); + +export { changeThreadSettingsSpec }; diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js --- a/lib/shared/dm-ops/dm-op-specs.js +++ b/lib/shared/dm-ops/dm-op-specs.js @@ -1,6 +1,7 @@ // @flow import { addMembersSpec } from './add-members-spec.js'; +import { changeThreadSettingsSpec } from './change-thread-settings-spec.js'; import { createSidebarSpec } from './create-sidebar-spec.js'; import { createThreadSpec } from './create-thread-spec.js'; import type { DMOperationSpec } from './dm-op-spec.js'; @@ -24,4 +25,5 @@ [dmOperationTypes.JOIN_THREAD]: joinThreadSpec, [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec, [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec, + [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec, }); diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -16,7 +16,9 @@ type NonSidebarThickThreadType, nonSidebarThickThreadTypes, } from './thread-types-enum.js'; +import type { ThreadChanges } from './thread-types.js'; import type { ClientUpdateInfo } from './update-types.js'; +import { threadSettingsChangesValidator } from './validators/thread-validators.js'; import { threadInfoValidator } from '../permissions/minimally-encoded-thread-permissions-validators.js'; import { values } from '../utils/objects.js'; import { tShape, tString, tUserID } from '../utils/validation-utils.js'; @@ -31,6 +33,7 @@ JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', + CHANGE_THREAD_SETTINGS: 'change_thread_settings', }); export type DMOperationType = $Values; @@ -222,6 +225,30 @@ removedUserIDs: t.list(tUserID), }); +export type DMChangeThreadSettingsOperation = { + +type: 'change_thread_settings', + +editorID: string, + +time: number, + +changes: ThreadChanges, + +messageIDsPrefix: string, + +threadInfo: ThickRawThreadInfo, + +rawMessageInfos: $ReadOnlyArray, + +truncationStatus: MessageTruncationStatus, + +rawEntryInfos: $ReadOnlyArray, +}; +export const dmChangeThreadSettingsOperation: TInterface = + tShape({ + type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), + editorID: tUserID, + time: t.Number, + changes: threadSettingsChangesValidator, + messageIDsPrefix: t.String, + threadInfo: threadInfoValidator, + rawMessageInfos: t.list(rawMessageInfoValidator), + truncationStatus: messageTruncationStatusValidator, + rawEntryInfos: t.list(rawEntryInfoValidator), + }); + export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation @@ -231,7 +258,8 @@ | DMAddMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation - | DMRemoveMembersOperation; + | DMRemoveMembersOperation + | DMChangeThreadSettingsOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, @@ -242,6 +270,7 @@ dmJoinThreadOperation, dmLeaveThreadOperation, dmRemoveMembersOperation, + dmChangeThreadSettingsOperation, ]); export type DMOperationResult = { diff --git a/lib/types/validators/thread-validators.js b/lib/types/validators/thread-validators.js --- a/lib/types/validators/thread-validators.js +++ b/lib/types/validators/thread-validators.js @@ -4,12 +4,21 @@ import type { TInterface } from 'tcomb'; import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; -import { tShape, tID } from '../../utils/validation-utils.js'; +import { updateUserAvatarRequestValidator } from '../../utils/avatar-utils.js'; +import { values } from '../../utils/objects.js'; +import { + tShape, + tID, + tNumEnum, + tColor, + tUserID, +} from '../../utils/validation-utils.js'; import { mediaValidator } from '../media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from '../message-types.js'; +import { threadTypes } from '../thread-types-enum.js'; import { type ChangeThreadSettingsResult, type LeaveThreadResult, @@ -19,6 +28,7 @@ type ToggleMessagePinResult, type RoleModificationResult, type RoleDeletionResult, + type ThreadChanges, } from '../thread-types.js'; import { serverUpdateInfoValidator } from '../update-types.js'; import { userInfosValidator } from '../user-types.js'; @@ -30,6 +40,17 @@ }), }); +export const threadSettingsChangesValidator: TInterface = + tShape({ + type: t.maybe(tNumEnum(values(threadTypes))), + name: t.maybe(t.String), + description: t.maybe(t.String), + color: t.maybe(tColor), + parentThreadID: t.maybe(tID), + newMemberIDs: t.maybe(t.list(tUserID)), + avatar: t.maybe(updateUserAvatarRequestValidator), + }); + export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator),