diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index 37d638465..3e5899f01 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,238 +1,235 @@ // @flow import type { TType } from 'tcomb'; import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion, - FUTURE_CODE_VERSION, + hasMinStateVersion, } from 'lib/shared/version-utils.js'; import { type PlatformDetails } from 'lib/types/device-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, - convertToNewIDSchema, keyserverPrefixID, convertClientIDsToServerIDs, convertObject, convertServerIDsToClientIDs, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; async function validateInput( viewer: Viewer, inputValidator: TType, input: mixed, ): Promise { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } const convertedInput = checkInputValidator(inputValidator, input); if ( - hasMinCodeVersion(viewer.platformDetails, { - native: FUTURE_CODE_VERSION, - web: FUTURE_CODE_VERSION, - }) && - convertToNewIDSchema + hasMinStateVersion(viewer.platformDetails, { + native: 43, + web: 3, + }) ) { try { return convertClientIDsToServerIDs( keyserverPrefixID, inputValidator, convertedInput, ); } catch (err) { throw new ServerError(err.message); } } return convertedInput; } function validateOutput( platformDetails: ?PlatformDetails, outputValidator: TType, data: T, alwaysConvertSchema?: boolean, ): T { if (!outputValidator.is(data)) { console.trace( 'Output validation failed, validator is:', outputValidator.displayName, ); return data; } if ( - (hasMinCodeVersion(platformDetails, { - native: FUTURE_CODE_VERSION, - web: FUTURE_CODE_VERSION, + hasMinStateVersion(platformDetails, { + native: 43, + web: 3, }) || - alwaysConvertSchema) && - convertToNewIDSchema + alwaysConvertSchema ) { return convertServerIDsToClientIDs( keyserverPrefixID, outputValidator, data, ); } return data; } function checkInputValidator(inputValidator: TType, input: mixed): T { if (inputValidator.is(input)) { return assertWithValidator(input, inputValidator); } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } return null; } async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, { native: 181 })) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, policiesValidator, }; diff --git a/lib/_generated/migration-utils.js b/lib/_generated/migration-utils.js index 693ac7c8c..98bbd1c04 100644 --- a/lib/_generated/migration-utils.js +++ b/lib/_generated/migration-utils.js @@ -1,914 +1,896 @@ // @flow -import type { EntryStore } from '../types/entry-types.js'; -import type { CalendarFilter } from '../types/filter-types.js'; -import type { InviteLinksStore } from '../types/link-types.js'; -import type { MessageStore, RawMessageInfo } from '../types/message-types.js'; -import type { ConnectionInfo } from '../types/socket-types.js'; -import type { RawThreadInfo } from '../types/thread-types.js'; import { entries } from '../utils/objects.js'; -export function convertRawMessageInfoToNewIDSchema( - input: RawMessageInfo, -): RawMessageInfo { +export function convertRawMessageInfoToNewIDSchema(input: any): any { return input.type === 0 ? { ...input, threadID: '256|' + input.threadID, id: input.id !== null && input.id !== undefined ? '256|' + input.id : input.id, } : input.type === 14 ? { ...input, threadID: '256|' + input.threadID, media: input.media.map(elem => ({ ...elem, id: '256|' + elem.id })), id: input.id !== null && input.id !== undefined ? '256|' + input.id : input.id, } : input.type === 15 ? { ...input, threadID: '256|' + input.threadID, media: input.media.map(elem => elem.type === 'photo' ? { ...elem, id: '256|' + elem.id } : elem.type === 'video' ? { ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, } : elem.type === 'encrypted_photo' ? ({ ...elem, id: '256|' + elem.id }: any) : elem.type === 'encrypted_video' ? ({ ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, }: any) : elem, ), id: input.id !== null && input.id !== undefined ? '256|' + input.id : input.id, } : input.type === 1 ? { ...input, threadID: '256|' + input.threadID, initialThreadState: { ...input.initialThreadState, parentThreadID: input.initialThreadState.parentThreadID !== null && input.initialThreadState.parentThreadID !== undefined ? '256|' + input.initialThreadState.parentThreadID : input.initialThreadState.parentThreadID, memberIDs: input.initialThreadState.memberIDs, }, id: '256|' + input.id, } : input.type === 2 ? { ...input, threadID: '256|' + input.threadID, addedUserIDs: input.addedUserIDs, id: '256|' + input.id, } : input.type === 3 ? { ...input, threadID: '256|' + input.threadID, childThreadID: '256|' + input.childThreadID, id: '256|' + input.id, } : input.type === 4 ? { ...input, threadID: '256|' + input.threadID, id: '256|' + input.id } : input.type === 5 ? { ...input, threadID: '256|' + input.threadID, removedUserIDs: input.removedUserIDs, id: '256|' + input.id, } : input.type === 6 ? { ...input, threadID: '256|' + input.threadID, userIDs: input.userIDs, newRole: '256|' + input.newRole, id: '256|' + input.id, } : input.type === 7 ? { ...input, threadID: '256|' + input.threadID, id: '256|' + input.id } : input.type === 8 ? { ...input, threadID: '256|' + input.threadID, id: '256|' + input.id } : input.type === 9 ? { ...input, threadID: '256|' + input.threadID, entryID: '256|' + input.entryID, id: '256|' + input.id, } : input.type === 10 ? { ...input, threadID: '256|' + input.threadID, entryID: '256|' + input.entryID, id: '256|' + input.id, } : input.type === 11 ? { ...input, threadID: '256|' + input.threadID, entryID: '256|' + input.entryID, id: '256|' + input.id, } : input.type === 12 ? { ...input, threadID: '256|' + input.threadID, entryID: '256|' + input.entryID, id: '256|' + input.id, } : input.type === 16 ? { ...input, threadID: '256|' + input.threadID, id: '256|' + input.id } : input.type === 18 ? { ...input, threadID: '256|' + input.threadID, initialThreadState: { ...input.initialThreadState, parentThreadID: '256|' + input.initialThreadState.parentThreadID, memberIDs: input.initialThreadState.memberIDs, }, id: '256|' + input.id, } : input.type === 13 ? { ...input, id: '256|' + input.id, threadID: '256|' + input.threadID } : input.type === 21 ? { ...input, threadID: '256|' + input.threadID, targetMessageID: '256|' + input.targetMessageID, id: '256|' + input.id, } : input.type === 17 ? { ...input, threadID: '256|' + input.threadID, sourceMessage: input.sourceMessage !== null && input.sourceMessage !== undefined ? input.sourceMessage.type === 0 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: input.sourceMessage.id !== null && input.sourceMessage.id !== undefined ? '256|' + input.sourceMessage.id : input.sourceMessage.id, } : input.sourceMessage.type === 14 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: input.sourceMessage.id !== null && input.sourceMessage.id !== undefined ? '256|' + input.sourceMessage.id : input.sourceMessage.id, media: input.sourceMessage.media.map(elem => ({ ...elem, id: '256|' + elem.id, })), } : input.sourceMessage.type === 15 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: input.sourceMessage.id !== null && input.sourceMessage.id !== undefined ? '256|' + input.sourceMessage.id : input.sourceMessage.id, media: input.sourceMessage.media.map(elem => elem.type === 'photo' ? { ...elem, id: '256|' + elem.id } : elem.type === 'video' ? { ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, } : elem.type === 'encrypted_photo' ? ({ ...elem, id: '256|' + elem.id }: any) : elem.type === 'encrypted_video' ? ({ ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, }: any) : elem, ), } : input.sourceMessage.type === 1 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, initialThreadState: { ...input.sourceMessage.initialThreadState, parentThreadID: input.sourceMessage.initialThreadState.parentThreadID !== null && input.sourceMessage.initialThreadState.parentThreadID !== undefined ? '256|' + input.sourceMessage.initialThreadState.parentThreadID : input.sourceMessage.initialThreadState.parentThreadID, memberIDs: input.sourceMessage.initialThreadState.memberIDs, }, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 2 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, addedUserIDs: input.sourceMessage.addedUserIDs, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 3 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, childThreadID: '256|' + input.sourceMessage.childThreadID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 4 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 5 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, removedUserIDs: input.sourceMessage.removedUserIDs, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 6 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, userIDs: input.sourceMessage.userIDs, newRole: '256|' + input.sourceMessage.newRole, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 7 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 8 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 9 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, entryID: '256|' + input.sourceMessage.entryID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 10 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, entryID: '256|' + input.sourceMessage.entryID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 11 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, entryID: '256|' + input.sourceMessage.entryID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 12 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, entryID: '256|' + input.sourceMessage.entryID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 16 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 18 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, initialThreadState: { ...input.sourceMessage.initialThreadState, parentThreadID: '256|' + input.sourceMessage.initialThreadState.parentThreadID, memberIDs: input.sourceMessage.initialThreadState.memberIDs, }, id: '256|' + input.sourceMessage.id, } : input.sourceMessage.type === 13 ? { ...input.sourceMessage, id: '256|' + input.sourceMessage.id, threadID: '256|' + input.sourceMessage.threadID, } : input.sourceMessage.type === 21 ? { ...input.sourceMessage, threadID: '256|' + input.sourceMessage.threadID, targetMessageID: '256|' + input.sourceMessage.targetMessageID, id: '256|' + input.sourceMessage.id, } : input.sourceMessage : input.sourceMessage, id: '256|' + input.id, } : input.type === 19 ? { ...input, threadID: '256|' + input.threadID, targetMessageID: '256|' + input.targetMessageID, id: input.id !== null && input.id !== undefined ? '256|' + input.id : input.id, } : input.type === 20 ? { ...input, threadID: '256|' + input.threadID, targetMessageID: '256|' + input.targetMessageID, id: '256|' + input.id, } : input; } -export function convertRawThreadInfoToNewIDSchema( - input: RawThreadInfo, -): RawThreadInfo { +export function convertRawThreadInfoToNewIDSchema(input: any): any { return { ...input, id: '256|' + input.id, parentThreadID: input.parentThreadID !== null && input.parentThreadID !== undefined ? '256|' + input.parentThreadID : input.parentThreadID, containingThreadID: input.containingThreadID !== null && input.containingThreadID !== undefined ? '256|' + input.containingThreadID : input.containingThreadID, community: input.community !== null && input.community !== undefined ? '256|' + input.community : input.community, members: input.members.map(elem => ({ ...elem, role: elem.role !== null && elem.role !== undefined ? '256|' + elem.role : elem.role, permissions: Object.fromEntries( entries(elem.permissions).map(([key, value]) => [ key, value.value === true ? { ...value, source: '256|' + value.source } : value, ]), ), })), roles: Object.fromEntries( entries(input.roles).map(([key, value]) => [ '256|' + key, { ...value, id: '256|' + value.id }, ]), ), currentUser: { ...input.currentUser, role: input.currentUser.role !== null && input.currentUser.role !== undefined ? '256|' + input.currentUser.role : input.currentUser.role, permissions: Object.fromEntries( entries(input.currentUser.permissions).map(([key, value]) => [ key, value.value === true ? { ...value, source: '256|' + value.source } : value, ]), ), }, sourceMessageID: input.sourceMessageID !== null && input.sourceMessageID !== undefined ? '256|' + input.sourceMessageID : input.sourceMessageID, }; } -export function convertMessageStoreToNewIDSchema( - input: MessageStore, -): MessageStore { +export function convertMessageStoreToNewIDSchema(input: any): any { return { ...input, messages: Object.fromEntries( entries(input.messages).map(([key, value]) => [ '256|' + key, value.type === 0 ? { ...value, threadID: '256|' + value.threadID, id: value.id !== null && value.id !== undefined ? '256|' + value.id : value.id, } : value.type === 14 ? { ...value, threadID: '256|' + value.threadID, media: value.media.map(elem => ({ ...elem, id: '256|' + elem.id, })), id: value.id !== null && value.id !== undefined ? '256|' + value.id : value.id, } : value.type === 15 ? { ...value, threadID: '256|' + value.threadID, media: value.media.map(elem => elem.type === 'photo' ? { ...elem, id: '256|' + elem.id } : elem.type === 'video' ? { ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, } : elem.type === 'encrypted_photo' ? ({ ...elem, id: '256|' + elem.id }: any) : elem.type === 'encrypted_video' ? ({ ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, }: any) : elem, ), id: value.id !== null && value.id !== undefined ? '256|' + value.id : value.id, } : value.type === 1 ? { ...value, threadID: '256|' + value.threadID, initialThreadState: { ...value.initialThreadState, parentThreadID: value.initialThreadState.parentThreadID !== null && value.initialThreadState.parentThreadID !== undefined ? '256|' + value.initialThreadState.parentThreadID : value.initialThreadState.parentThreadID, memberIDs: value.initialThreadState.memberIDs, }, id: '256|' + value.id, } : value.type === 2 ? { ...value, threadID: '256|' + value.threadID, addedUserIDs: value.addedUserIDs, id: '256|' + value.id, } : value.type === 3 ? { ...value, threadID: '256|' + value.threadID, childThreadID: '256|' + value.childThreadID, id: '256|' + value.id, } : value.type === 4 ? { ...value, threadID: '256|' + value.threadID, id: '256|' + value.id, } : value.type === 5 ? { ...value, threadID: '256|' + value.threadID, removedUserIDs: value.removedUserIDs, id: '256|' + value.id, } : value.type === 6 ? { ...value, threadID: '256|' + value.threadID, userIDs: value.userIDs, newRole: '256|' + value.newRole, id: '256|' + value.id, } : value.type === 7 ? { ...value, threadID: '256|' + value.threadID, id: '256|' + value.id, } : value.type === 8 ? { ...value, threadID: '256|' + value.threadID, id: '256|' + value.id, } : value.type === 9 ? { ...value, threadID: '256|' + value.threadID, entryID: '256|' + value.entryID, id: '256|' + value.id, } : value.type === 10 ? { ...value, threadID: '256|' + value.threadID, entryID: '256|' + value.entryID, id: '256|' + value.id, } : value.type === 11 ? { ...value, threadID: '256|' + value.threadID, entryID: '256|' + value.entryID, id: '256|' + value.id, } : value.type === 12 ? { ...value, threadID: '256|' + value.threadID, entryID: '256|' + value.entryID, id: '256|' + value.id, } : value.type === 16 ? { ...value, threadID: '256|' + value.threadID, id: '256|' + value.id, } : value.type === 18 ? { ...value, threadID: '256|' + value.threadID, initialThreadState: { ...value.initialThreadState, parentThreadID: '256|' + value.initialThreadState.parentThreadID, memberIDs: value.initialThreadState.memberIDs, }, id: '256|' + value.id, } : value.type === 13 ? { ...value, id: '256|' + value.id, threadID: '256|' + value.threadID, } : value.type === 21 ? { ...value, threadID: '256|' + value.threadID, targetMessageID: '256|' + value.targetMessageID, id: '256|' + value.id, } : value.type === 17 ? { ...value, threadID: '256|' + value.threadID, sourceMessage: value.sourceMessage !== null && value.sourceMessage !== undefined ? value.sourceMessage.type === 0 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: value.sourceMessage.id !== null && value.sourceMessage.id !== undefined ? '256|' + value.sourceMessage.id : value.sourceMessage.id, } : value.sourceMessage.type === 14 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: value.sourceMessage.id !== null && value.sourceMessage.id !== undefined ? '256|' + value.sourceMessage.id : value.sourceMessage.id, media: value.sourceMessage.media.map(elem => ({ ...elem, id: '256|' + elem.id, })), } : value.sourceMessage.type === 15 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: value.sourceMessage.id !== null && value.sourceMessage.id !== undefined ? '256|' + value.sourceMessage.id : value.sourceMessage.id, media: value.sourceMessage.media.map(elem => elem.type === 'photo' ? { ...elem, id: '256|' + elem.id } : elem.type === 'video' ? { ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, } : elem.type === 'encrypted_photo' ? ({ ...elem, id: '256|' + elem.id }: any) : elem.type === 'encrypted_video' ? ({ ...elem, id: '256|' + elem.id, thumbnailID: '256|' + elem.thumbnailID, }: any) : elem, ), } : value.sourceMessage.type === 1 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, initialThreadState: { ...value.sourceMessage.initialThreadState, parentThreadID: value.sourceMessage.initialThreadState .parentThreadID !== null && value.sourceMessage.initialThreadState .parentThreadID !== undefined ? '256|' + value.sourceMessage.initialThreadState .parentThreadID : value.sourceMessage.initialThreadState .parentThreadID, memberIDs: value.sourceMessage.initialThreadState.memberIDs, }, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 2 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, addedUserIDs: value.sourceMessage.addedUserIDs, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 3 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, childThreadID: '256|' + value.sourceMessage.childThreadID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 4 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 5 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, removedUserIDs: value.sourceMessage.removedUserIDs, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 6 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, userIDs: value.sourceMessage.userIDs, newRole: '256|' + value.sourceMessage.newRole, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 7 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 8 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 9 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, entryID: '256|' + value.sourceMessage.entryID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 10 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, entryID: '256|' + value.sourceMessage.entryID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 11 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, entryID: '256|' + value.sourceMessage.entryID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 12 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, entryID: '256|' + value.sourceMessage.entryID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 16 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 18 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, initialThreadState: { ...value.sourceMessage.initialThreadState, parentThreadID: '256|' + value.sourceMessage.initialThreadState .parentThreadID, memberIDs: value.sourceMessage.initialThreadState.memberIDs, }, id: '256|' + value.sourceMessage.id, } : value.sourceMessage.type === 13 ? { ...value.sourceMessage, id: '256|' + value.sourceMessage.id, threadID: '256|' + value.sourceMessage.threadID, } : value.sourceMessage.type === 21 ? { ...value.sourceMessage, threadID: '256|' + value.sourceMessage.threadID, targetMessageID: '256|' + value.sourceMessage.targetMessageID, id: '256|' + value.sourceMessage.id, } : value.sourceMessage : value.sourceMessage, id: '256|' + value.id, } : value.type === 19 ? { ...value, threadID: '256|' + value.threadID, targetMessageID: '256|' + value.targetMessageID, id: value.id !== null && value.id !== undefined ? '256|' + value.id : value.id, } : value.type === 20 ? { ...value, threadID: '256|' + value.threadID, targetMessageID: '256|' + value.targetMessageID, id: '256|' + value.id, } : value, ]), ), threads: Object.fromEntries( entries(input.threads).map(([key, value]) => [ '256|' + key, { ...value, messageIDs: value.messageIDs.map(elem => '256|' + elem) }, ]), ), }; } -export function convertEntryStoreToNewIDSchema(input: EntryStore): EntryStore { +export function convertEntryStoreToNewIDSchema(input: any): any { return { ...input, entryInfos: Object.fromEntries( entries(input.entryInfos).map(([key, value]) => [ '256|' + key, { ...value, id: value.id !== null && value.id !== undefined ? '256|' + value.id : value.id, threadID: '256|' + value.threadID, }, ]), ), daysToEntries: Object.fromEntries( entries(input.daysToEntries).map(([key, value]) => [ key, value.map(elem => '256|' + elem), ]), ), }; } -export function convertInviteLinksStoreToNewIDSchema( - input: InviteLinksStore, -): InviteLinksStore { +export function convertInviteLinksStoreToNewIDSchema(input: any): any { return { ...input, links: Object.fromEntries( entries(input.links).map(([key, value]) => [ '256|' + key, { ...value, primaryLink: value.primaryLink !== null && value.primaryLink !== undefined ? { ...value.primaryLink, role: '256|' + value.primaryLink.role, communityID: '256|' + value.primaryLink.communityID, } : value.primaryLink, }, ]), ), }; } -export function convertCalendarFilterToNewIDSchema( - input: CalendarFilter, -): CalendarFilter { +export function convertCalendarFilterToNewIDSchema(input: any): any { return input.type === 'threads' ? { ...input, threadIDs: input.threadIDs.map(elem => '256|' + elem) } : input; } -export function convertConnectionInfoToNewIDSchema( - input: ConnectionInfo, -): ConnectionInfo { +export function convertConnectionInfoToNewIDSchema(input: any): any { return { ...input, queuedActivityUpdates: input.queuedActivityUpdates.map(elem => ({ ...elem, threadID: '256|' + elem.threadID, latestMessage: elem.latestMessage !== null && elem.latestMessage !== undefined ? '256|' + elem.latestMessage : elem.latestMessage, })), actualizedCalendarQuery: { ...input.actualizedCalendarQuery, filters: input.actualizedCalendarQuery.filters.map(elem => elem.type === 'threads' ? { ...elem, threadIDs: elem.threadIDs.map(elem2 => '256|' + elem2) } : elem, ), }, }; } diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index 7c7f6b4a8..04ed6afe4 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,257 +1,252 @@ // @flow import { createSelector } from 'reselect'; import t from 'tcomb'; import { currentCalendarQuery } from './nav-selectors.js'; import { serverEntryInfo, serverEntryInfosObject, filterRawEntryInfosByCalendarQuery, } from '../shared/entry-utils.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type RawEntryInfo, rawEntryInfoValidator, type CalendarQuery, } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; import type { OneTimeKeyGenerator } from '../types/socket-types.js'; import { type RawThreadInfo, rawThreadInfoValidator, } from '../types/thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type UserInfos, userInfosValidator, } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; import { minimumOneTimeKeysRequired } from '../utils/crypto-utils.js'; import { values, hash } from '../utils/objects.js'; import { tID, convertClientIDsToServerIDs, keyserverPrefixID, - convertToNewIDSchema, } from '../utils/validation-utils.js'; const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); const getClientResponsesSelector: ( state: AppState, ) => ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (state: AppState) => state.threadStore.threadInfos, (state: AppState) => state.entryStore.entryInfos, (state: AppState) => state.userStore.userInfos, (state: AppState) => state.currentUserInfo, currentCalendarQuery, ( threadInfos: { +[id: string]: RawThreadInfo }, entryInfos: { +[id: string]: RawEntryInfo }, userInfos: UserInfos, currentUserInfo: ?CurrentUserInfo, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { - if (convertToNewIDSchema) { - threadInfos = convertClientIDsToServerIDs( - keyserverPrefixID, - t.dict(tID, rawThreadInfoValidator), - threadInfos, - ); - userInfos = convertClientIDsToServerIDs( - keyserverPrefixID, - userInfosValidator, - userInfos, - ); - currentUserInfo = convertClientIDsToServerIDs( - keyserverPrefixID, - t.maybe(currentUserInfoValidator), - currentUserInfo, - ); - } + threadInfos = convertClientIDsToServerIDs( + keyserverPrefixID, + t.dict(tID, rawThreadInfoValidator), + threadInfos, + ); + userInfos = convertClientIDsToServerIDs( + keyserverPrefixID, + userInfosValidator, + userInfos, + ); + currentUserInfo = convertClientIDsToServerIDs( + keyserverPrefixID, + t.maybe(currentUserInfoValidator), + currentUserInfo, + ); return async ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { let filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery(calendarActive), ); - if (convertToNewIDSchema) { - filteredEntryInfos = convertClientIDsToServerIDs( - keyserverPrefixID, - t.dict(tID, rawEntryInfoValidator), - filteredEntryInfos, - ); - } + filteredEntryInfos = convertClientIDsToServerIDs( + keyserverPrefixID, + t.dict(tID, rawEntryInfoValidator), + filteredEntryInfos, + ); const hashResults = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; if (key === 'threadInfos') { hashValue = hash(threadInfos); } else if (key === 'entryInfos') { hashValue = hash(filteredEntryInfos); } else if (key === 'userInfos') { hashValue = hash(userInfos); } else if (key === 'currentUserInfo') { hashValue = hash(currentUserInfo); } else if (key.startsWith('threadInfo|')) { const [, threadID] = key.split('|'); hashValue = hash(threadInfos[threadID]); } else if (key.startsWith('entryInfo|')) { const [, entryID] = key.split('|'); let rawEntryInfo = filteredEntryInfos[entryID]; if (rawEntryInfo) { rawEntryInfo = serverEntryInfo(rawEntryInfo); } hashValue = hash(rawEntryInfo); } else if (key.startsWith('userInfo|')) { const [, userID] = key.split('|'); hashValue = hash(userInfos[userID]); } else { continue; } hashResults[key] = expectedHashValue === hashValue; } const { failUnmentioned } = serverRequest; if (failUnmentioned && failUnmentioned.threadInfos) { for (const threadID in threadInfos) { const key = `threadInfo|${threadID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.entryInfos) { for (const entryID in filteredEntryInfos) { const key = `entryInfo|${entryID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.userInfos) { for (const userID in userInfos) { const key = `userInfo|${userID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); } else if ( serverRequest.type === serverRequestTypes.MORE_ONE_TIME_KEYS && oneTimeKeyGenerator ) { const keys: string[] = []; for (let i = 0; i < minimumOneTimeKeysRequired; ++i) { keys.push(oneTimeKeyGenerator(i)); } clientResponses.push({ type: serverRequestTypes.MORE_ONE_TIME_KEYS, keys, }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB && getSignedIdentityKeysBlob ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && getInitialNotificationsEncryptedMessage ) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage(); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const sessionStateFuncSelector: ( state: AppState, ) => (calendarActive: boolean) => SessionState = createSelector( (state: AppState) => state.messageStore.currentAsOf, (state: AppState) => state.updatesCurrentAsOf, currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); export { queuedReports, getClientResponsesSelector, sessionStateFuncSelector }; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js index 3d1456791..efaa98341 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,250 +1,248 @@ // @flow import invariant from 'invariant'; import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import t from 'tcomb'; import type { TStructProps, TIrreducible, TRefinement, TEnums, TInterface, TUnion, TType, } from 'tcomb'; import { validEmailRegex, oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, PhotoMessageServerDBContent, VideoMessageServerDBContent, } from '../types/messages/media'; function tBool(value: boolean): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tString(value: string): TIrreducible { return t.irreducible(`'${value}'`, x => x === value); } function tNumber(value: number): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tShape(spec: TStructProps): TInterface { return t.interface(spec, { strict: true }); } type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); } function tNumEnum(nums: $ReadOnlyArray): TRefinement { return t.refinement(t.Number, (input: number) => { for (const num of nums) { if (input === num) { return true; } } return false; }); } const tNull: TIrreducible = t.irreducible('null', x => x === null); const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char const tPlatform: TEnums = t.enums.of([ 'ios', 'android', 'web', 'windows', 'macos', ]); const tDeviceType: TEnums = t.enums.of(['ios', 'android']); const tPlatformDetails: TInterface = tShape({ platform: tPlatform, codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), }); const tPassword: TRefinement = t.refinement( t.String, (password: string) => !!password, ); const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); const tEmail: TRegex = tRegex(validEmailRegex); const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); const tMediaMessagePhoto: TInterface = tShape({ type: tString('photo'), uploadID: tID, }); const tMediaMessageVideo: TInterface = tShape({ type: tString('video'), uploadID: tID, thumbnailUploadID: tID, }); const tMediaMessageMedia: TUnion = t.union([ tMediaMessagePhoto, tMediaMessageVideo, ]); function assertWithValidator(data: mixed, validator: TType): T { invariant(validator.is(data), "data isn't of type T"); return (data: any); } -const convertToNewIDSchema = false; const keyserverPrefixID = '256'; function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = id => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } return `${serverPrefixID}|${id}`; }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = id => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } throw new Error('invalid_client_id_prefix'); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } export { tBool, tString, tNumber, tShape, tRegex, tNumEnum, tNull, tDate, tColor, tPlatform, tDeviceType, tPlatformDetails, tPassword, tCookie, tEmail, tOldValidUsername, tID, tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, assertWithValidator, - convertToNewIDSchema, keyserverPrefixID, convertClientIDsToServerIDs, convertServerIDsToClientIDs, convertObject, }; diff --git a/native/redux/persist.js b/native/redux/persist.js index c8305ef66..17da2b72a 100644 --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -1,765 +1,765 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import invariant from 'invariant'; import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createTransform } from 'redux-persist'; import type { Transform } from 'redux-persist/es/types.js'; import { convertEntryStoreToNewIDSchema, convertInviteLinksStoreToNewIDSchema, convertMessageStoreToNewIDSchema, convertRawMessageInfoToNewIDSchema, convertCalendarFilterToNewIDSchema, convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; import { type ReportStoreOperation, type ClientDBReportStoreOperation, convertReportStoreOperationToClientDBReportStoreOperation, convertReportsToReplaceReportOps, } from 'lib/ops/report-store-ops.js'; import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js'; import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js'; import { getContainingThreadID, getCommunity, } from 'lib/shared/thread-utils.js'; import { DEPRECATED_unshimMessageStore, unshimFunc, } from 'lib/shared/unshim-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { type LocalMessageInfo, type MessageStore, type ClientDBMessageStoreOperation, } from 'lib/types/message-types.js'; import type { ReportStore, ClientReportCreationRequest, } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import type { ClientDBThreadStoreOperation, ClientDBThreadInfo, } from 'lib/types/thread-types.js'; import { convertMessageStoreOperationsToClientDBOperations, translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, convertThreadStoreOperationsToClientDBOperations, } from 'lib/utils/thread-ops-utils.js'; import { getUUID } from 'lib/utils/uuid.js'; import { keyserverPrefixID } from 'lib/utils/validation-utils.js'; import { updateClientDBThreadStoreThreadInfos, createUpdateDBOpsForThreadStoreThreadInfos, createUpdateDBOpsForMessageStoreMessages, createUpdateDBOpsForMessageStoreThreads, } from './client-db-utils.js'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js'; import { persistMigrationForManagePinsThreadPermission } from './manage-pins-permission-migration.js'; import type { AppState } from './state-types.js'; import { unshimClientDB } from './unshim-utils.js'; import { updateRolesAndPermissions } from './update-roles-and-permissions.js'; import { commCoreModule } from '../native-modules.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultGlobalThemeInfo } from '../types/themes.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; const migrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, }), [2]: (state: AppState) => ({ ...state, messageSentFromRoute: [], }), [3]: state => ({ currentUserInfo: state.currentUserInfo, entryStore: state.entryStore, threadInfos: state.threadInfos, userInfos: state.userInfos, messageStore: { ...state.messageStore, currentAsOf: state.currentAsOf, }, updatesCurrentAsOf: state.currentAsOf, cookie: state.cookie, deviceToken: state.deviceToken, urlPrefix: state.urlPrefix, customServer: state.customServer, notifPermissionAlertInfo: state.notifPermissionAlertInfo, messageSentFromRoute: state.messageSentFromRoute, _persist: state._persist, }), [4]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, }), [5]: (state: AppState) => ({ ...state, calendarFilters: defaultCalendarFilters, }), [6]: state => ({ ...state, threadInfos: undefined, threadStore: { threadInfos: state.threadInfos, inconsistencyResponses: [], }, }), [7]: state => ({ ...state, lastUserInteraction: undefined, sessionID: undefined, entryStore: { ...state.entryStore, inconsistencyResponses: [], }, }), [8]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, connection: defaultConnectionInfo(Platform.OS), watchedThreadIDs: [], entryStore: { ...state.entryStore, actualizedCalendarQuery: undefined, }, }), [9]: (state: AppState) => ({ ...state, connection: { ...state.connection, lateResponses: [], }, }), [10]: (state: AppState) => ({ ...state, nextLocalID: highestLocalIDSelector(state) + 1, connection: { ...state.connection, showDisconnectedBar: false, }, messageStore: { ...state.messageStore, local: {}, }, }), [11]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.IMAGES, ]), }), [12]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [13]: (state: AppState) => ({ ...state, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), }), [14]: (state: AppState) => state, [15]: state => ({ ...state, threadStore: { ...state.threadStore, inconsistencyReports: inconsistencyResponsesToReports( state.threadStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: inconsistencyResponsesToReports( state.entryStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, queuedReports: [], }), [16]: state => { const result = { ...state, messageSentFromRoute: undefined, dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous, }; if (state.navInfo) { result.navInfo = { ...state.navInfo, navigationState: undefined, }; } return result; }, [17]: state => ({ ...state, userInfos: undefined, userStore: { userInfos: state.userInfos, inconsistencyResponses: [], }, }), [18]: state => ({ ...state, userStore: { userInfos: state.userStore.userInfos, inconsistencyReports: [], }, }), [19]: state => { const threadInfos = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const { visibilityRules, ...rest } = threadInfo; threadInfos[threadID] = rest; } return { ...state, threadStore: { ...state.threadStore, threadInfos, }, }; }, [20]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.UPDATE_RELATIONSHIP, ]), }), [21]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.CREATE_SIDEBAR, messageTypes.SIDEBAR_SOURCE, ]), }), [22]: state => { for (const key in state.drafts) { const value = state.drafts[key]; try { commCoreModule.updateDraft(key, value); } catch (e) { if (!isTaskCancelledError(e)) { throw e; } } } return { ...state, drafts: undefined, }; }, [23]: state => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [24]: state => ({ ...state, enabledApps: defaultEnabledApps, }), [25]: state => ({ ...state, crashReportsEnabled: __DEV__, }), [26]: state => { const { currentUserInfo } = state; if (currentUserInfo.anonymous) { return state; } return { ...state, crashReportsEnabled: undefined, currentUserInfo: { id: currentUserInfo.id, username: currentUserInfo.username, }, enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, }; }, [27]: state => ({ ...state, queuedReports: undefined, enabledReports: undefined, threadStore: { ...state.threadStore, inconsistencyReports: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: undefined, }, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [ ...state.entryStore.inconsistencyReports, ...state.threadStore.inconsistencyReports, ...state.queuedReports, ], }, }), [28]: state => { const threadParentToChildren = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? state.threadStore.threadInfos[threadInfo.parentThreadID] : null; const parentIndex = parentThreadInfo ? parentThreadInfo.id : '-1'; if (!threadParentToChildren[parentIndex]) { threadParentToChildren[parentIndex] = []; } threadParentToChildren[parentIndex].push(threadID); } const rootIDs = threadParentToChildren['-1']; if (!rootIDs) { // This should never happen, but if it somehow does we'll let the state // check mechanism resolve it... return state; } const threadInfos = {}; const stack = [...rootIDs]; while (stack.length > 0) { const threadID = stack.shift(); const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; threadInfos[threadID] = { ...threadInfo, containingThreadID: getContainingThreadID( parentThreadInfo, threadInfo.type, ), community: getCommunity(parentThreadInfo), }; const children = threadParentToChildren[threadID]; if (children) { stack.push(...children); } } return { ...state, threadStore: { ...state.threadStore, threadInfos } }; }, [29]: (state: AppState) => { const updatedThreadInfos = migrateThreadStoreForEditThreadPermissions( state.threadStore.threadInfos, ); return { ...state, threadStore: { ...state.threadStore, threadInfos: updatedThreadInfos, }, }; }, [30]: (state: AppState) => { const threadInfos = state.threadStore.threadInfos; const operations = [ { type: 'remove_all', }, ...Object.keys(threadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: threadInfos[id] }, })), ]; try { commCoreModule.processThreadStoreOperationsSync( convertThreadStoreOperationsToClientDBOperations(operations), ); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [31]: (state: AppState) => { const messages = state.messageStore.messages; const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...Object.keys(messages).map((id: string) => ({ type: 'replace', payload: translateRawMessageInfoToClientDBMessageInfo(messages[id]), })), ]; try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [32]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [33]: (state: AppState) => unshimClientDB(state, [messageTypes.REACTION]), [34]: state => { const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state; return stateSansThreadIDsToNotifIDs; }, [35]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [36]: (state: AppState) => { // 1. Get threads and messages from SQLite `threads` and `messages` tables. const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); // 2. Translate `ClientDBThreadInfo`s to `RawThreadInfo`s and // `ClientDBMessageInfo`s to `RawMessageInfo`s. const rawThreadInfos = clientDBThreadInfos.map( convertClientDBThreadInfoToRawThreadInfo, ); const rawMessageInfos = clientDBMessageInfos.map( translateClientDBMessageInfoToRawMessageInfo, ); // 3. Unshim translated `RawMessageInfos` to get the TOGGLE_PIN messages const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => unshimFunc(messageInfo, new Set([messageTypes.TOGGLE_PIN])), ); // 4. Filter out non-TOGGLE_PIN messages const filteredRawMessageInfos = unshimmedRawMessageInfos.filter( messageInfo => messageInfo.type === messageTypes.TOGGLE_PIN, ); // 5. We want only the last TOGGLE_PIN message for each message ID, // so 'pin', 'unpin', 'pin' don't count as 3 pins, but only 1. const lastMessageIDToRawMessageInfoMap = new Map(); for (const messageInfo of filteredRawMessageInfos) { const { targetMessageID } = messageInfo; lastMessageIDToRawMessageInfoMap.set(targetMessageID, messageInfo); } const lastMessageIDToRawMessageInfos = Array.from( lastMessageIDToRawMessageInfoMap.values(), ); // 6. Create a Map of threadIDs to pinnedCount const threadIDsToPinnedCount = new Map(); for (const messageInfo of lastMessageIDToRawMessageInfos) { const { threadID, type } = messageInfo; if (type === messageTypes.TOGGLE_PIN) { const pinnedCount = threadIDsToPinnedCount.get(threadID) || 0; threadIDsToPinnedCount.set(threadID, pinnedCount + 1); } } // 7. Include a pinnedCount for each rawThreadInfo const rawThreadInfosWithPinnedCount = rawThreadInfos.map(threadInfo => ({ ...threadInfo, pinnedCount: threadIDsToPinnedCount.get(threadInfo.id) || 0, })); // 8. Convert rawThreadInfos to a map of threadID to threadInfo const threadIDToThreadInfo = rawThreadInfosWithPinnedCount.reduce( (acc, threadInfo) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); // 9. Add threadPermission to each threadInfo const rawThreadInfosWithThreadPermission = persistMigrationForManagePinsThreadPermission(threadIDToThreadInfo); // 10. Convert the new threadInfos back into an array const rawThreadInfosWithCountAndPermission = Object.keys( rawThreadInfosWithThreadPermission, ).map(id => rawThreadInfosWithThreadPermission[id]); // 11. Translate `RawThreadInfo`s to `ClientDBThreadInfo`s. const convertedClientDBThreadInfos = rawThreadInfosWithCountAndPermission.map( convertRawThreadInfoToClientDBThreadInfo, ); // 12. Construct `ClientDBThreadStoreOperation`s to clear SQLite `threads` // table and repopulate with `ClientDBThreadInfo`s. const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; // 13. Try processing `ClientDBThreadStoreOperation`s and log out if // `processThreadStoreOperationsSync(...)` throws an exception. try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return state; }, [37]: state => { const operations = convertMessageStoreOperationsToClientDBOperations([ { type: 'remove_all_threads', }, { type: 'replace_threads', payload: { threads: state.messageStore.threads }, }, ]); try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.error(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [38]: state => updateClientDBThreadStoreThreadInfos(state, updateRolesAndPermissions), [39]: (state: AppState) => unshimClientDB(state, [messageTypes.EDIT_MESSAGE]), [40]: state => updateClientDBThreadStoreThreadInfos(state, updateRolesAndPermissions), [41]: (state: AppState) => { const queuedReports = state.reportStore.queuedReports.map(report => ({ ...report, id: getUUID(), })); return { ...state, reportStore: { ...state.reportStore, queuedReports }, }; }, [42]: (state: AppState) => { const reportStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_reports' }, ...convertReportsToReplaceReportOps(state.reportStore.queuedReports), ]; const dbOperations: $ReadOnlyArray = convertReportStoreOperationToClientDBReportStoreOperation( reportStoreOperations, ); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [43]: async (state: AppState) => { const { messages, drafts, threads, messageStoreThreads } = await commCoreModule.getClientDBStore(); const messageStoreThreadsOperations = createUpdateDBOpsForMessageStoreThreads( messageStoreThreads, convertMessageStoreThreadsToNewIDSchema, ); const messageStoreMessagesOperations = createUpdateDBOpsForMessageStoreMessages(messages, messageInfos => messageInfos.map(convertRawMessageInfoToNewIDSchema), ); const threadOperations = createUpdateDBOpsForThreadStoreThreadInfos( threads, convertThreadStoreThreadInfosToNewIDSchema, ); const draftOperations = generateIDSchemaMigrationOpsForDrafts(drafts); try { await Promise.all([ commCoreModule.processMessageStoreOperations([ ...messageStoreMessagesOperations, ...messageStoreThreadsOperations, ]), commCoreModule.processThreadStoreOperations(threadOperations), commCoreModule.processDraftStoreOperations(draftOperations), ]); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return { ...state, entryStore: convertEntryStoreToNewIDSchema(state.entryStore), messageStore: convertMessageStoreToNewIDSchema(state.messageStore), calendarFilters: state.calendarFilters.map( convertCalendarFilterToNewIDSchema, ), connection: convertConnectionInfoToNewIDSchema(state.connection), watchedThreadIDs: state.watchedThreadIDs.map( id => `${keyserverPrefixID}|${id}`, ), inviteLinksStore: convertInviteLinksStoreToNewIDSchema( state.inviteLinksStore, ), }; }, }; // After migration 31, we'll no longer want to persist `messageStore.messages` // via redux-persist. However, we DO want to continue persisting everything in // `messageStore` EXCEPT for `messages`. The `blacklist` property in // `persistConfig` allows us to specify top-level keys that shouldn't be // persisted. However, we aren't able to specify nested keys in `blacklist`. // As a result, if we want to prevent nested keys from being persisted we'll // need to use `createTransform(...)` to specify an `inbound` function that // allows us to modify the `state` object before it's passed through // `JSON.stringify(...)` and written to disk. We specify the keys for which // this transformation should be executed in the `whitelist` property of the // `config` object that's passed to `createTransform(...)`. // eslint-disable-next-line no-unused-vars type PersistedThreadMessageInfo = { +startReached: boolean, +lastNavigatedTo: number, +lastPruned: number, }; type PersistedMessageStore = { +local: { +[id: string]: LocalMessageInfo }, +currentAsOf: number, +threads: { +[threadID: string]: PersistedThreadMessageInfo }, }; const messageStoreMessagesBlocklistTransform: Transform = createTransform( (state: MessageStore): PersistedMessageStore => { const { messages, threads, ...messageStoreSansMessages } = state; // We also do not want to persist `messageStore.threads[ID].messageIDs` // because they can be deterministically computed based on messages we have // from SQLite const threadsToPersist = {}; for (const threadID in threads) { const { messageIDs, ...threadsData } = threads[threadID]; threadsToPersist[threadID] = threadsData; } return { ...messageStoreSansMessages, threads: threadsToPersist }; }, (state: MessageStore): MessageStore => { const { threads: persistedThreads, ...messageStore } = state; const threads = {}; for (const threadID in persistedThreads) { threads[threadID] = { ...persistedThreads[threadID], messageIDs: [] }; } // We typically expect `messageStore.messages` to be `undefined` because // messages are persisted in the SQLite `messages` table rather than via // `redux-persist`. In this case we want to set `messageStore.messages` // to {} so we don't run into issues with `messageStore.messages` being // `undefined` (https://phab.comm.dev/D5545). // // However, in the case that a user is upgrading from a client where // `persistConfig.version` < 31, we expect `messageStore.messages` to // contain messages stored via `redux-persist` that we need in order // to correctly populate the SQLite `messages` table in migration 31 // (https://phab.comm.dev/D2600). // // However, because `messageStoreMessagesBlocklistTransform` modifies // `messageStore` before migrations are run, we need to make sure we aren't // inadvertently clearing `messageStore.messages` (by setting to {}) before // messages are stored in SQLite (https://linear.app/comm/issue/ENG-2377). return { ...messageStore, threads, messages: messageStore.messages ?? {} }; }, { whitelist: ['messageStore'] }, ); type PersistedReportStore = $Diff< ReportStore, { +queuedReports: $ReadOnlyArray }, >; const reportStoreTransform: Transform = createTransform( (state: ReportStore): PersistedReportStore => { return { enabledReports: state.enabledReports }; }, (state: PersistedReportStore): ReportStore => { return { ...state, queuedReports: [] }; }, { whitelist: ['reportStore'] }, ); const persistConfig = { key: 'root', storage: AsyncStorage, blacklist: [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'draftStore', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', 'storeLoaded', ], debug: __DEV__, - version: 42, + version: 43, transforms: [messageStoreMessagesBlocklistTransform, reportStoreTransform], migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void), }; const codeVersion: number = commCoreModule.getCodeVersion(); // This local exists to avoid a circular dependency where redux-setup needs to // import all the navigation and screen stuff, but some of those screens want to // access the persistor to purge its state. let storedPersistor = null; function setPersistor(persistor: *) { storedPersistor = persistor; } function getPersistor(): empty { invariant(storedPersistor, 'should be set'); return storedPersistor; } export { persistConfig, codeVersion, setPersistor, getPersistor }; diff --git a/web/redux/persist.js b/web/redux/persist.js index ab97a94ed..fc01b9675 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,143 +1,143 @@ // @flow import invariant from 'invariant'; import { getStoredState, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { createAsyncMigrate, type StorageMigrationFunction, } from 'lib/shared/create-async-migrate.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, } from 'lib/utils/migration-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import type { AppState } from './redux-setup.js'; import { databaseModule } from '../database/database-module-provider.js'; import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var preloadedState: AppState; const initiallyLoggedInUserID = preloadedState.currentUserInfo?.anonymous ? undefined : preloadedState.currentUserInfo?.id; const isDatabaseSupported = isSQLiteSupported(initiallyLoggedInUserID); const migrations = { [1]: async state => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async state => { if (!isDatabaseSupported) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, [3]: async (state: AppState) => { let newState = state; if (state.draftStore) { newState = { ...newState, draftStore: convertDraftStoreToNewIDSchema(state.draftStore), }; } if (!isDatabaseSupported) { return newState; } const stores = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); invariant(stores?.store, 'Stores should exist'); await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( stores.store.drafts, ), }, }); return newState; }, }; const persistWhitelist = [ 'enabledApps', 'deviceID', 'cryptoStore', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'lastCommunicatedPlatformDetails', ]; const rootKey = 'root'; const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } return oldStorage; }; const persistConfig: PersistConfig = { key: rootKey, storage: commReduxStorageEngine, whitelist: isDatabaseSupported ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( migrations, { debug: isDev }, migrateStorageToSQLite, ): any), - version: 2, + version: 3, }; export { persistConfig };