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/facts/genesis.js b/lib/facts/genesis.js index 099d0e2e7..6bf2b749b 100644 --- a/lib/facts/genesis.js +++ b/lib/facts/genesis.js @@ -1,25 +1,23 @@ // @flow -import { convertToNewIDSchema } from '../utils/validation-utils.js'; - type Genesis = { +id: string, +name: string, +description: string, +introMessages: $ReadOnlyArray, }; const genesis: Genesis = { - id: process.env['KEYSERVER'] || !convertToNewIDSchema ? '1' : '256|1', + id: process.env['KEYSERVER'] ? '1' : '256|1', name: 'GENESIS', description: 'This is the first community on Comm. In the future it will be possible to create chats outside of a community, but for now all of these chats get set with GENESIS as their parent. GENESIS is hosted on Ashoat’s keyserver.', introMessages: [ 'welcome to Genesis!', 'for now, Genesis is the only community on Comm, and is the parent of all new chats', 'this is meant to be temporary. we’re working on support for chats that can exist outside of any community, as well as support for user-hosted communities', 'to learn more about our roadmap and how Genesis fits in, check out [this document](https://www.notion.so/Comm-Genesis-1059f131fb354250abd1966894b15951)', ], }; export default genesis; 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 };