diff --git a/keyserver/src/scripts/generate-converter-from-validator.js b/keyserver/src/scripts/generate-converter-from-validator.js index 8a6588ef9..0d0769dbe 100644 --- a/keyserver/src/scripts/generate-converter-from-validator.js +++ b/keyserver/src/scripts/generate-converter-from-validator.js @@ -1,234 +1,234 @@ // @flow import t, { type TInterface, type TType } from 'tcomb'; import { nativeMediaSelectionValidator, mediaValidator, } from 'lib/types/media-types.js'; import { threadPermissionInfoValidator } from 'lib/types/thread-permission-types.js'; -import { legacyRawThreadInfoValidator } from 'lib/types/thread-types.js'; +import { legacyThinRawThreadInfoValidator } from 'lib/types/thread-types.js'; import { ashoatKeyserverID, tID } from 'lib/utils/validation-utils.js'; import { main } from './utils.js'; function getDiscriminatorFieldForUnionValidator(validator: TType) { if (validator === threadPermissionInfoValidator) { return 'value'; } if (validator === nativeMediaSelectionValidator) { return 'step'; } return 'type'; } function flattenInnerUnionValidators( innerValidators: $ReadOnlyArray>, ): TInterface<{ +[string]: mixed }>[] { let result: TInterface<{ +[string]: mixed }>[] = []; for (const innerValidator of innerValidators) { if (innerValidator.meta.kind === 'interface') { // In flow, union refinement only works if every variant has a key // that is a literal. In this case we don't get a refinement of // `innerValidator` because we are checking value of the inner // `meta` object. const recastValidator: TInterface<{ +[string]: mixed }> = (innerValidator: any); result.push(recastValidator); } else if (innerValidator.meta.kind === 'union') { result = [ ...result, ...flattenInnerUnionValidators(innerValidator.meta.types), ]; } else if ([t.String, t.Number, t.Boolean].includes(innerValidator)) { // We don't need to handle literal types because they can't be // converted } else { throw new Error( `Validator not supported in union: ${innerValidator.displayName}`, ); } } return result; } // MediaValidator is special cased because of flow issues function getConverterForMediaValidator(inputName: string) { return `(${inputName}.type === 'photo' ? { ...${inputName}, id: '256|' + ${inputName}.id } : ${inputName}.type === 'video' ? { ...${inputName}, id: '256|' + ${inputName}.id, thumbnailID: '256|' + ${inputName}.thumbnailID, } : ${inputName}.type === 'encrypted_photo' ? ({ ...${inputName}, id: '256|' + ${inputName}.id }: any) : ${inputName}.type === 'encrypted_video' ? ({ ...${inputName}, id: '256|' + ${inputName}.id, thumbnailID: '256|' + ${inputName}.thumbnailID, }: any) : ${inputName})`; } // `null` is returned if there is no conversion needed in T or any // of it's inner types function generateConverterFromValidator( validator: TType, inputName: string, validatorToBeConverted: TType, conversionExpressionString: (inputName: string) => string, ): ?string { if (validator === validatorToBeConverted) { return `(${conversionExpressionString(inputName)})`; } else if (validator === mediaValidator) { return getConverterForMediaValidator(inputName); } if (validator.meta.kind === 'maybe') { const inner = generateConverterFromValidator( validator.meta.type, inputName, validatorToBeConverted, conversionExpressionString, ); if (!inner) { return null; } return `((${inputName} !== null && ${inputName} !== undefined) ? (${inner}) : (${inputName}))`; } if (validator.meta.kind === 'subtype') { return generateConverterFromValidator( validator.meta.type, inputName, validatorToBeConverted, conversionExpressionString, ); } if (validator.meta.kind === 'interface') { // In flow, union refinement only works if every variant has a key // that is a literal. In this case we don't get a refinement of `validator` // because we are checking value of the inner `meta` object. const recastValidator: TInterface<{ +[string]: mixed }> = (validator: any); const fieldConverters = []; for (const key in recastValidator.meta.props) { const inner = generateConverterFromValidator( recastValidator.meta.props[key], `${inputName}.${key}`, validatorToBeConverted, conversionExpressionString, ); if (inner) { fieldConverters.push(`${key}:${inner}`); } } if (fieldConverters.length === 0) { return null; } return `({...${inputName}, ${fieldConverters.join(',')}})`; } if (validator.meta.kind === 'union') { const innerValidators = flattenInnerUnionValidators(validator.meta.types); const variantConverters = []; for (const innerValidator of innerValidators) { const discriminatorField = getDiscriminatorFieldForUnionValidator(validator); const discriminatorValidator = innerValidator.meta.props[discriminatorField]; if (!discriminatorValidator) { throw new Error( 'Union should have a discriminator ' + validator.displayName, ); } const discriminatorValue = discriminatorValidator.meta.name; const inner = generateConverterFromValidator( innerValidator, inputName, validatorToBeConverted, conversionExpressionString, ); if (inner) { variantConverters.push( `(${inputName}.${discriminatorField} === ${discriminatorValue}) ? (${inner})`, ); } } if (variantConverters.length === 0) { return null; } variantConverters.push(`(${inputName})`); return `(${variantConverters.join(':')})`; } if (validator.meta.kind === 'list') { const inner = generateConverterFromValidator( validator.meta.type, 'elem', validatorToBeConverted, conversionExpressionString, ); if (!inner) { return inputName; } return `(${inputName}.map(elem => ${inner}))`; } if (validator.meta.kind === 'dict') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; let domainConverter = null; if (domainValidator === validatorToBeConverted) { domainConverter = conversionExpressionString('key'); } let codomainConverter = generateConverterFromValidator( codomainValidator, 'value', validatorToBeConverted, conversionExpressionString, ); if (!domainConverter && !codomainConverter) { return null; } domainConverter = domainConverter ?? 'key'; codomainConverter = codomainConverter ?? 'value'; return `(Object.fromEntries( entries(${inputName}).map( ([key, value]) => [${domainConverter}, ${codomainConverter}] ) ))`; } return null; } // Input arguments: -const validator = legacyRawThreadInfoValidator; +const validator = legacyThinRawThreadInfoValidator; const typeName = 'RawThreadInfo'; const validatorToBeConverted = tID; const conversionExpressionString = (inputName: string) => `'${ashoatKeyserverID}|' + ${inputName}`; main([ async () => { console.log( `export function convert${typeName}ToNewIDSchema(input: ${typeName}): ${typeName} { return`, generateConverterFromValidator( validator, 'input', validatorToBeConverted, conversionExpressionString, ) ?? 'input', ';}', ); }, ]); diff --git a/keyserver/src/shared/state-sync/threads-state-sync-spec.js b/keyserver/src/shared/state-sync/threads-state-sync-spec.js index ae0faefe5..4a4358d29 100644 --- a/keyserver/src/shared/state-sync/threads-state-sync-spec.js +++ b/keyserver/src/shared/state-sync/threads-state-sync-spec.js @@ -1,52 +1,56 @@ // @flow -import { mixedRawThreadInfoValidator } from 'lib/permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from 'lib/permissions/minimally-encoded-raw-thread-info-validators.js'; import { threadsStateSyncSpec as libSpec } from 'lib/shared/state-sync/threads-state-sync-spec.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { ClientThreadInconsistencyReportCreationRequest } from 'lib/types/report-types.js'; import { type MixedRawThreadInfos, type LegacyRawThreadInfo, } from 'lib/types/thread-types.js'; import { hash, combineUnorderedHashes, values } from 'lib/utils/objects.js'; import type { ServerStateSyncSpec } from './state-sync-spec.js'; import { fetchThreadInfos } from '../../fetchers/thread-fetchers.js'; import type { Viewer } from '../../session/viewer.js'; import { validateOutput } from '../../utils/validation-utils.js'; export const threadsStateSyncSpec: ServerStateSyncSpec< MixedRawThreadInfos, MixedRawThreadInfos, LegacyRawThreadInfo | RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ fetch, async fetchFullSocketSyncPayload(viewer: Viewer) { const result = await fetchThreadInfos(viewer); return result.threadInfos; }, async fetchServerInfosHash(viewer: Viewer, ids?: $ReadOnlySet) { const infos = await fetch(viewer, ids); return await getServerInfosHash(infos); }, getServerInfosHash, getServerInfoHash, ...libSpec, }); async function fetch(viewer: Viewer, ids?: $ReadOnlySet) { const filter = ids ? { threadIDs: ids } : undefined; const result = await fetchThreadInfos(viewer, filter); return result.threadInfos; } async function getServerInfosHash(infos: MixedRawThreadInfos) { const results = await Promise.all(values(infos).map(getServerInfoHash)); return combineUnorderedHashes(results); } async function getServerInfoHash(info: LegacyRawThreadInfo | RawThreadInfo) { - const output = await validateOutput(null, mixedRawThreadInfoValidator, info); + const output = await validateOutput( + null, + mixedThinRawThreadInfoValidator, + info, + ); return hash(output); } diff --git a/lib/permissions/minimally-encoded-raw-thread-info-validators.js b/lib/permissions/minimally-encoded-raw-thread-info-validators.js index 770f80c9f..ef18566ca 100644 --- a/lib/permissions/minimally-encoded-raw-thread-info-validators.js +++ b/lib/permissions/minimally-encoded-raw-thread-info-validators.js @@ -1,135 +1,136 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { tHexEncodedPermissionsBitmask, tHexEncodedRolePermission, } from './minimally-encoded-thread-permissions.js'; import { specialRoleValidator } from './special-roles.js'; import type { MemberInfoWithPermissions, ThreadCurrentUserInfo, - RawThreadInfo, + ThinRawThreadInfo, RoleInfo, RoleInfoWithoutSpecialRole, - RawThreadInfoWithoutSpecialRole, + ThinRawThreadInfoWithoutSpecialRole, MinimallyEncodedThickMemberInfo, MemberInfoSansPermissions, } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadSubscriptionValidator } from '../types/subscription-types.js'; import { - type LegacyRawThreadInfo, + type LegacyThinRawThreadInfo, legacyMemberInfoValidator, - legacyRawThreadInfoValidator, + legacyThinRawThreadInfoValidator, legacyThreadCurrentUserInfoValidator, } from '../types/thread-types.js'; import { tBool, tID, tShape, tUserID } from '../utils/validation-utils.js'; const threadCurrentUserInfoValidator: TInterface = tShape({ ...legacyThreadCurrentUserInfoValidator.meta.props, minimallyEncoded: tBool(true), permissions: tHexEncodedPermissionsBitmask, }); const roleInfoValidatorBase = { id: tID, name: t.String, minimallyEncoded: tBool(true), permissions: t.list(tHexEncodedRolePermission), }; const roleInfoValidator: TInterface = tShape({ ...roleInfoValidatorBase, specialRole: t.maybe(specialRoleValidator), }); type RoleInfoPossiblyWithIsDefaultField = $ReadOnly<{ ...RoleInfo, +isDefault?: boolean, }>; // This validator is to be used in `convertClientDBThreadInfoToRawThreadInfo` // which validates the persisted JSON blob BEFORE any migrations are run. // `roleInfoValidator` will fail for persisted `RoleInfo`s that include // the `isDefault` field. Figured it made sense to create a separate validator // instead of adding complexity to `roleInfoValidator` which should maintain // 1:1 correspondance with the `RoleInfo` type. const persistedRoleInfoValidator: TInterface = tShape({ id: tID, name: t.String, minimallyEncoded: tBool(true), permissions: t.list(tHexEncodedRolePermission), specialRole: t.maybe(specialRoleValidator), isDefault: t.maybe(t.Boolean), }); const memberInfoWithPermissionsValidator: TInterface = tShape({ ...legacyMemberInfoValidator.meta.props, minimallyEncoded: tBool(true), permissions: tHexEncodedPermissionsBitmask, }); const memberInfoSansPermissionsValidator: TInterface = tShape({ id: tUserID, role: t.maybe(tID), isSender: t.Boolean, minimallyEncoded: tBool(true), }); const minimallyEncodedThickMemberInfoValidator: TInterface = tShape({ minimallyEncoded: tBool(true), id: tUserID, role: t.maybe(tID), permissions: tHexEncodedPermissionsBitmask, isSender: t.Boolean, subscription: threadSubscriptionValidator, }); -const rawThreadInfoValidator: TInterface = tShape( - { - ...legacyRawThreadInfoValidator.meta.props, +const thinRawThreadInfoValidator: TInterface = + tShape({ + ...legacyThinRawThreadInfoValidator.meta.props, minimallyEncoded: tBool(true), members: t.union([ t.list(memberInfoWithPermissionsValidator), t.list(memberInfoSansPermissionsValidator), ]), roles: t.dict(tID, roleInfoValidator), currentUser: threadCurrentUserInfoValidator, - }, -); + }); const roleInfoWithoutSpecialRolesValidator: TInterface = tShape({ ...roleInfoValidatorBase, isDefault: t.maybe(t.Boolean), }); -const rawThreadInfoWithoutSpecialRoles: TInterface = - tShape({ - ...rawThreadInfoValidator.meta.props, +const thinRawThreadInfoWithoutSpecialRolesValidator: TInterface = + tShape({ + ...thinRawThreadInfoValidator.meta.props, roles: t.dict(tID, roleInfoWithoutSpecialRolesValidator), }); -const mixedRawThreadInfoValidator: TUnion< - LegacyRawThreadInfo | RawThreadInfo | RawThreadInfoWithoutSpecialRole, +const mixedThinRawThreadInfoValidator: TUnion< + | LegacyThinRawThreadInfo + | ThinRawThreadInfo + | ThinRawThreadInfoWithoutSpecialRole, > = t.union([ - legacyRawThreadInfoValidator, - rawThreadInfoValidator, - rawThreadInfoWithoutSpecialRoles, + legacyThinRawThreadInfoValidator, + thinRawThreadInfoValidator, + thinRawThreadInfoWithoutSpecialRolesValidator, ]); export { memberInfoWithPermissionsValidator, memberInfoSansPermissionsValidator, minimallyEncodedThickMemberInfoValidator, roleInfoValidator, persistedRoleInfoValidator, threadCurrentUserInfoValidator, - rawThreadInfoValidator, - mixedRawThreadInfoValidator, + thinRawThreadInfoValidator, + mixedThinRawThreadInfoValidator, }; diff --git a/lib/permissions/minimally-encoded-thread-permissions.test.js b/lib/permissions/minimally-encoded-thread-permissions.test.js index 2d7f2a519..f809ec979 100644 --- a/lib/permissions/minimally-encoded-thread-permissions.test.js +++ b/lib/permissions/minimally-encoded-thread-permissions.test.js @@ -1,671 +1,671 @@ // @flow import { memberInfoWithPermissionsValidator, persistedRoleInfoValidator, - rawThreadInfoValidator, + thinRawThreadInfoValidator, roleInfoValidator, threadCurrentUserInfoValidator, } from './minimally-encoded-raw-thread-info-validators.js'; import { exampleMinimallyEncodedRawThreadInfoA, exampleRawThreadInfoA, expectedDecodedExampleRawThreadInfoA, } from './minimally-encoded-thread-permissions-test-data.js'; import { decodeRolePermissionBitmask, decodeThreadRolePermissionsBitmaskArray, hasPermission, permissionsToBitmaskHex, rolePermissionToBitmaskHex, threadPermissionsFromBitmaskHex, threadRolePermissionsBlobToBitmaskArray, } from './minimally-encoded-thread-permissions.js'; import { specialRoles } from './special-roles.js'; import { minimallyEncodeRawThreadInfoWithMemberPermissions, deprecatedDecodeMinimallyEncodedRawThreadInfo, minimallyEncodeThreadCurrentUserInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import type { ThreadRolePermissionsBlob } from '../types/thread-permission-types.js'; import type { LegacyThreadCurrentUserInfo } from '../types/thread-types.js'; const permissions = { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, voiced: { value: true, source: '1' }, edit_entries: { value: true, source: '1' }, edit_thread: { value: true, source: '1' }, edit_thread_description: { value: true, source: '1' }, edit_thread_color: { value: true, source: '1' }, delete_thread: { value: true, source: '1' }, create_subthreads: { value: true, source: '1' }, create_sidebars: { value: true, source: '1' }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: true, source: '1' }, remove_members: { value: true, source: '1' }, change_role: { value: true, source: '1' }, leave_thread: { value: false, source: null }, react_to_message: { value: true, source: '1' }, edit_message: { value: true, source: '1' }, edit_thread_avatar: { value: false, source: null }, manage_pins: { value: false, source: null }, manage_invite_links: { value: false, source: null }, voiced_in_announcement_channels: { value: false, source: null }, manage_farcaster_channel_tags: { value: false, source: null }, }; describe('minimallyEncodedThreadPermissions', () => { it('should encode ThreadPermissionsInfo as bitmask', () => { const permissionsBitmask = permissionsToBitmaskHex(permissions); expect(permissionsBitmask).toBe('373ff'); expect(hasPermission(permissionsBitmask, 'know_of')).toBe(true); expect(hasPermission(permissionsBitmask, 'visible')).toBe(true); expect(hasPermission(permissionsBitmask, 'voiced')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_entries')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_thread')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_thread_description')).toBe( true, ); expect(hasPermission(permissionsBitmask, 'edit_thread_color')).toBe(true); expect(hasPermission(permissionsBitmask, 'delete_thread')).toBe(true); expect(hasPermission(permissionsBitmask, 'create_subthreads')).toBe(true); expect(hasPermission(permissionsBitmask, 'create_sidebars')).toBe(true); expect(hasPermission(permissionsBitmask, 'join_thread')).toBe(false); expect(hasPermission(permissionsBitmask, 'edit_permissions')).toBe(false); expect(hasPermission(permissionsBitmask, 'remove_members')).toBe(true); expect(hasPermission(permissionsBitmask, 'change_role')).toBe(true); expect(hasPermission(permissionsBitmask, 'leave_thread')).toBe(false); expect(hasPermission(permissionsBitmask, 'react_to_message')).toBe(true); expect(hasPermission(permissionsBitmask, 'edit_message')).toBe(true); }); }); describe('hasPermission', () => { const permissionsSansKnowOf = { know_of: { value: false, source: null }, visible: { value: true, source: '1' }, }; const permissionsSansKnowOfBitmask = permissionsToBitmaskHex( permissionsSansKnowOf, ); it('should fail check if know_of is false even if permission specified in request is true', () => { expect(hasPermission(permissionsSansKnowOfBitmask, 'visible')).toBe(false); }); const permissionsWithKnowOf = { know_of: { value: true, source: '1' }, visible: { value: true, source: '1' }, }; const permissionsWithKnowOfBitmask = permissionsToBitmaskHex( permissionsWithKnowOf, ); it('should succeed permission check if know_of is true', () => { expect(hasPermission(permissionsWithKnowOfBitmask, 'visible')).toBe(true); }); }); describe('threadPermissionsFromBitmaskHex', () => { const expectedDecodedThreadPermissions = { know_of: { value: true, source: 'null' }, visible: { value: true, source: 'null' }, voiced: { value: true, source: 'null' }, edit_entries: { value: true, source: 'null' }, edit_thread: { value: true, source: 'null' }, edit_thread_description: { value: true, source: 'null' }, edit_thread_color: { value: true, source: 'null' }, delete_thread: { value: true, source: 'null' }, create_subthreads: { value: true, source: 'null' }, create_sidebars: { value: true, source: 'null' }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: true, source: 'null' }, remove_members: { value: true, source: 'null' }, change_role: { value: true, source: 'null' }, leave_thread: { value: false, source: null }, react_to_message: { value: true, source: 'null' }, edit_message: { value: true, source: 'null' }, edit_thread_avatar: { value: false, source: null }, manage_pins: { value: false, source: null }, manage_invite_links: { value: false, source: null }, voiced_in_announcement_channels: { value: false, source: null }, manage_farcaster_channel_tags: { value: false, source: null }, }; it('should decode ThreadPermissionsInfo from bitmask', () => { const permissionsBitmask = permissionsToBitmaskHex(permissions); const decodedThreadPermissions = threadPermissionsFromBitmaskHex(permissionsBitmask); expect(decodedThreadPermissions).toStrictEqual( expectedDecodedThreadPermissions, ); }); it('should decode bitmask strings under 3 characters', () => { // We know that '3' in hex is 0b0011. Given that permissions are encoded // from least significant bit (LSB) to most significant bit (MSB), we would // except this to mean that only the first two permissions listed in // `baseRolePermissionEncoding` are `true`. Which is the case. const decodedThreadPermissions = threadPermissionsFromBitmaskHex('3'); expect(decodedThreadPermissions).toStrictEqual({ know_of: { value: true, source: 'null' }, visible: { value: true, source: 'null' }, voiced: { value: false, source: null }, edit_entries: { value: false, source: null }, edit_thread: { value: false, source: null }, edit_thread_description: { value: false, source: null }, edit_thread_color: { value: false, source: null }, delete_thread: { value: false, source: null }, create_subthreads: { value: false, source: null }, create_sidebars: { value: false, source: null }, join_thread: { value: false, source: null }, edit_permissions: { value: false, source: null }, add_members: { value: false, source: null }, remove_members: { value: false, source: null }, change_role: { value: false, source: null }, leave_thread: { value: false, source: null }, react_to_message: { value: false, source: null }, edit_message: { value: false, source: null }, edit_thread_avatar: { value: false, source: null }, manage_pins: { value: false, source: null }, manage_invite_links: { value: false, source: null }, voiced_in_announcement_channels: { value: false, source: null }, manage_farcaster_channel_tags: { value: false, source: null }, }); }); }); describe('rolePermissionToBitmaskHex', () => { it('should encode `child_opentoplevel_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_visible')).toBe( '01b', ); }); it('should encode `child_opentoplevel_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_know_of')).toBe( '00b', ); }); it('should encode `child_toplevel_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_toplevel_visible')).toBe('01a'); }); it('should encode `child_toplevel_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_toplevel_know_of')).toBe('00a'); }); it('should encode `child_opentoplevel_join_thread` successfully', () => { expect(rolePermissionToBitmaskHex('child_opentoplevel_join_thread')).toBe( '0ab', ); }); it('should encode `child_visible` successfully', () => { expect(rolePermissionToBitmaskHex('child_visible')).toBe('018'); }); it('should encode `child_know_of` successfully', () => { expect(rolePermissionToBitmaskHex('child_know_of')).toBe('008'); }); }); describe('decodeRolePermissionBitmask', () => { it('should decode `01b` to `child_opentoplevel_visible` successfully', () => { expect(decodeRolePermissionBitmask('01b')).toBe( 'child_opentoplevel_visible', ); }); it('should decode `00b` to `child_opentoplevel_know_of` successfully', () => { expect(decodeRolePermissionBitmask('00b')).toBe( 'child_opentoplevel_know_of', ); }); it('should decode `01a` to `child_toplevel_visible` successfully', () => { expect(decodeRolePermissionBitmask('01a')).toBe('child_toplevel_visible'); }); it('should decode `00a` to `child_toplevel_know_of` successfully', () => { expect(decodeRolePermissionBitmask('00a')).toBe('child_toplevel_know_of'); }); it('should decode `0ab` to `child_opentoplevel_join_thread` successfully', () => { expect(decodeRolePermissionBitmask('0ab')).toBe( 'child_opentoplevel_join_thread', ); }); it('should decode `018` to `child_visible` successfully', () => { expect(decodeRolePermissionBitmask('018')).toBe('child_visible'); }); it('should decode `008` to `child_know_of` successfully', () => { expect(decodeRolePermissionBitmask('008')).toBe('child_know_of'); }); }); const threadRolePermissionsBlob: ThreadRolePermissionsBlob = { add_members: true, child_open_join_thread: true, create_sidebars: true, create_subthreads: true, descendant_open_know_of: true, descendant_open_visible: true, descendant_opentoplevel_join_thread: true, edit_entries: true, edit_message: true, edit_permissions: true, edit_thread: true, edit_thread_avatar: true, edit_thread_color: true, edit_thread_description: true, know_of: true, leave_thread: true, react_to_message: true, remove_members: true, visible: true, voiced: true, open_know_of: true, open_visible: true, opentoplevel_join_thread: true, toplevel_know_of: true, toplevel_visible: true, opentoplevel_know_of: true, opentoplevel_visible: true, child_know_of: true, child_visible: true, child_opentoplevel_join_thread: true, child_toplevel_know_of: true, child_toplevel_visible: true, child_opentoplevel_know_of: true, child_opentoplevel_visible: true, }; const threadRolePermissionsBitmaskArray = [ '0c0', '0a9', '090', '080', '005', '015', '0a7', '030', '110', '0b0', '040', '120', '060', '050', '000', '0f0', '100', '0d0', '010', '020', '001', '011', '0a3', '002', '012', '003', '013', '008', '018', '0ab', '00a', '01a', '00b', '01b', ]; describe('threadRolePermissionsBlobToBitmaskArray', () => { it('should encode threadRolePermissionsBlob as bitmask array', () => { const arr = threadRolePermissionsBlobToBitmaskArray( threadRolePermissionsBlob, ); expect(arr).toEqual(threadRolePermissionsBitmaskArray); }); }); describe('decodeThreadRolePermissionsBitmaskArray', () => { it('should decode threadRolePermissionsBitmaskArray', () => { expect( decodeThreadRolePermissionsBitmaskArray( threadRolePermissionsBitmaskArray, ), ).toEqual(threadRolePermissionsBlob); }); }); describe('minimallyEncodedRoleInfoValidator', () => { it('should validate correctly formed MinimallyEncodedRoleInfo', () => { expect( roleInfoValidator.is({ minimallyEncoded: true, id: 'roleID', name: 'roleName', permissions: ['abc', 'def'], specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(true); }); it('should NOT validate malformed MinimallyEncodedRoleInfo', () => { expect( roleInfoValidator.is({ id: 1234, name: 'roleName', permissions: ['abc', 'def'], isDefault: true, specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(false); expect( roleInfoValidator.is({ id: 'roleID', name: 'roleName', permissions: ['hello a02 test', 'def'], isDefault: true, specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(false); expect( roleInfoValidator.is({ id: 'roleID', name: 'roleName', permissions: [123, 456], isDefault: true, specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(false); expect( roleInfoValidator.is({ id: 'roleID', name: 'roleName', permissions: ['ZZZ', 'YYY'], isDefault: true, specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(false); expect( roleInfoValidator.is({ id: 'roleID', name: 'roleName', permissions: ['AAAAA', 'YYY'], isDefault: true, specialRole: specialRoles.DEFAULT_ROLE, }), ).toBe(false); }); }); describe('persistedRoleInfoValidator', () => { it('should validate persisted RoleInfo with isDefault field', () => { expect( persistedRoleInfoValidator.is({ minimallyEncoded: true, id: 'roleID', name: 'roleName', permissions: ['abc', 'def'], specialRole: specialRoles.DEFAULT_ROLE, isDefault: true, }), ).toBe(true); }); }); describe('minimallyEncodedThreadCurrentUserInfoValidator', () => { it('should validate correctly formed MinimallyEncodedThreadCurrentUserInfo', () => { expect( threadCurrentUserInfoValidator.is({ minimallyEncoded: true, permissions: '100', subscription: { home: true, pushNotifs: true }, }), ).toBe(true); expect( threadCurrentUserInfoValidator.is({ minimallyEncoded: true, permissions: 'ABCDEFABCDEFABCD', subscription: { home: true, pushNotifs: true }, }), ).toBe(true); }); it('should NOT validate malformed MinimallyEncodedThreadCurrentUserInfo', () => { expect( threadCurrentUserInfoValidator.is({ minimallyEncoded: true, permissions: 'INVALID', subscription: { home: true, pushNotifs: true }, }), ).toBe(false); expect( threadCurrentUserInfoValidator.is({ minimallyEncoded: true, permissions: 'ABCDEF hello ABCDEFABCD', subscription: { home: true, pushNotifs: true }, }), ).toBe(false); expect( threadCurrentUserInfoValidator.is({ minimallyEncoded: true, permissions: 100, subscription: { home: true, pushNotifs: true }, }), ).toBe(false); }); }); describe('minimallyEncodedMemberInfoValidator', () => { it('should validate correctly formed MinimallyEncodedMemberInfo', () => { expect( memberInfoWithPermissionsValidator.is({ minimallyEncoded: true, id: 'memberID', permissions: 'ABCDEF', isSender: true, }), ).toBe(true); expect( memberInfoWithPermissionsValidator.is({ minimallyEncoded: true, id: 'memberID', permissions: '01b', isSender: false, }), ).toBe(true); }); it('should NOT validate malformed MinimallyEncodedMemberInfo', () => { expect( memberInfoWithPermissionsValidator.is({ minimallyEncoded: true, id: 'memberID', permissions: 'INVALID', isSender: false, }), ).toBe(false); expect( memberInfoWithPermissionsValidator.is({ minimallyEncoded: true, id: 'memberID', permissions: 100, isSender: false, }), ).toBe(false); }); }); describe('minimallyEncodedRawThreadInfoValidator', () => { it('should validate correctly formed MinimallyEncodedRawThreadInfo', () => { expect( - rawThreadInfoValidator.is(exampleMinimallyEncodedRawThreadInfoA), + thinRawThreadInfoValidator.is(exampleMinimallyEncodedRawThreadInfoA), ).toBe(true); }); }); describe('minimallyEncodeRawThreadInfo', () => { it('should correctly encode RawThreadInfo', () => { expect( - rawThreadInfoValidator.is( + thinRawThreadInfoValidator.is( minimallyEncodeRawThreadInfoWithMemberPermissions( exampleRawThreadInfoA, ), ), ).toBe(true); }); }); describe('decodeMinimallyEncodedRawThreadInfo', () => { it('should correctly decode minimallyEncodedRawThreadInfo', () => { expect( deprecatedDecodeMinimallyEncodedRawThreadInfo( minimallyEncodeRawThreadInfoWithMemberPermissions( exampleRawThreadInfoA, ), ), ).toStrictEqual(expectedDecodedExampleRawThreadInfoA); }); }); const threadCurrentUserInfo: LegacyThreadCurrentUserInfo = { role: '256|83795', permissions: { know_of: { value: true, source: '256|1', }, visible: { value: true, source: '256|1', }, voiced: { value: false, source: null, }, edit_entries: { value: false, source: null, }, edit_thread: { value: false, source: null, }, edit_thread_description: { value: false, source: null, }, edit_thread_color: { value: false, source: null, }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: false, source: null, }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: false, source: null, }, edit_message: { value: false, source: null, }, edit_thread_avatar: { value: false, source: null, }, manage_pins: { value: false, source: null, }, manage_invite_links: { value: false, source: null, }, voiced_in_announcement_channels: { value: false, source: null, }, manage_farcaster_channel_tags: { value: false, source: null, }, }, subscription: { home: true, pushNotifs: true, }, unread: true, }; describe('minimallyEncodeThreadCurrentUserInfo', () => { it('should correctly encode threadCurrentUserInfo ONCE', () => { const minimallyEncoded = minimallyEncodeThreadCurrentUserInfo( threadCurrentUserInfo, ); expect(minimallyEncoded.permissions).toBe('3'); }); it('should throw when attempting to minimally encode threadCurrentUserInfo twice', () => { const minimallyEncoded = minimallyEncodeThreadCurrentUserInfo( threadCurrentUserInfo, ); expect(minimallyEncoded.permissions).toBe('3'); expect(() => // `MinimallyEncodedThreadCurrentUser` should never be passed // to `minimallyEncodeThreadCurrentUserInfo`. We're intentionally // bypassing Flow to simulate a scenario where malformed input is // passed to minimallyEncodeThreadCurrentUserInfo to ensure that the // `invariant` throws the expected error. // $FlowExpectedError minimallyEncodeThreadCurrentUserInfo(minimallyEncoded), ).toThrow('threadCurrentUserInfo is already minimally encoded.'); }); }); diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js index 273f4ef24..39c0088fa 100644 --- a/lib/shared/updates/join-thread-spec.js +++ b/lib/shared/updates/join-thread-spec.js @@ -1,184 +1,184 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; -import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import { type RawEntryInfo, rawEntryInfoValidator, } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../../types/message-types.js'; import { messageTruncationStatusValidator, rawMessageInfoValidator, } from '../../types/message-types.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { threadInFilterList } from '../thread-utils.js'; import { combineTruncationStatuses } from '../truncation-utils.js'; export const joinThreadSpec: UpdateSpec< ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadJoinUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } invariant( update.threadInfo.minimallyEncoded, 'update threadInfo must be minimallyEncoded', ); return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, mergeEntryInfos( entryIDs: Set, mergedEntryInfos: Array, update: ThreadJoinUpdateInfo, ) { for (const entryInfo of update.rawEntryInfos) { const entryID = entryInfo.id; if (!entryID || entryIDs.has(entryID)) { continue; } mergedEntryInfos.push(entryInfo); entryIDs.add(entryID); } }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadJoinUpdateInfo, ) { if ( !threadInFilterList(update.threadInfo) || filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set([...filteredThreadIDs, update.threadInfo.id]); }, getRawMessageInfos(update: ThreadJoinUpdateInfo) { return update.rawMessageInfos; }, mergeMessageInfosAndTruncationStatuses( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: ThreadJoinUpdateInfo, ) { for (const messageInfo of update.rawMessageInfos) { const messageID = messageInfo.id; if (!messageID || messageIDs.has(messageID)) { continue; } messageInfos.push(messageInfo); messageIDs.add(messageID); } truncationStatuses[update.threadInfo.id] = combineTruncationStatuses( update.truncationStatus, truncationStatuses[update.threadInfo.id], ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.JOIN_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadJoinUpdateData) { const { threadID } = data; return JSON.stringify({ threadID }); }, entitiesToFetch(update: ThreadJoinRawUpdateInfo) { return { threadID: update.threadID, detailedThreadID: update.threadID, }; }, rawInfoFromData(data: ThreadJoinUpdateData, id: string) { return { type: updateTypes.JOIN_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadJoinRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const { data, rawEntryInfosByThreadID, rawMessageInfosByThreadID } = params; const { threadInfos, calendarResult, messageInfosResult } = data; const threadInfo = threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.JOIN_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } invariant(calendarResult, 'should be set'); const rawEntryInfos = rawEntryInfosByThreadID[info.threadID] ?? []; invariant(messageInfosResult, 'should be set'); const rawMessageInfos = rawMessageInfosByThreadID[info.threadID] ?? []; return { type: updateTypes.JOIN_THREAD, id: info.id, time: info.time, threadInfo, rawMessageInfos, truncationStatus: messageInfosResult.truncationStatuses[info.threadID], rawEntryInfos, }; }, deleteCondition: 'all_types', keyForUpdateData(data: ThreadJoinUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadJoinUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: 'all_types', infoValidator: tShape({ type: tNumber(updateTypes.JOIN_THREAD), id: t.String, time: t.Number, - threadInfo: mixedRawThreadInfoValidator, + threadInfo: mixedThinRawThreadInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatus: messageTruncationStatusValidator, rawEntryInfos: t.list(rawEntryInfoValidator), }), getUpdatedThreadInfo(update: ThreadJoinUpdateInfo) { return update.threadInfo; }, }); diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js index 0389de77d..f0bc8422e 100644 --- a/lib/shared/updates/update-thread-spec.js +++ b/lib/shared/updates/update-thread-spec.js @@ -1,125 +1,125 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; -import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { threadInFilterList } from '../thread-utils.js'; export const updateThreadSpec: UpdateSpec< ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } invariant( update.threadInfo.minimallyEncoded, 'update threadInfo must be minimallyEncoded', ); return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadUpdateInfo, ) { if ( threadInFilterList(update.threadInfo) || !filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set( [...filteredThreadIDs].filter(id => id !== update.threadInfo.id), ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadUpdateData) { return JSON.stringify({ threadID: data.threadID }); }, entitiesToFetch(update: ThreadRawUpdateInfo) { return { threadID: update.threadID, }; }, rawInfoFromData(data: ThreadUpdateData, id: string) { return { type: updateTypes.UPDATE_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const threadInfo = params.data.threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.UPDATE_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } return { type: updateTypes.UPDATE_THREAD, id: info.id, time: info.time, threadInfo, }; }, deleteCondition: new Set([ updateTypes.UPDATE_THREAD, updateTypes.UPDATE_THREAD_READ_STATUS, ]), keyForUpdateData(data: ThreadUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: new Set([ updateTypes.UPDATE_THREAD_READ_STATUS, ]), infoValidator: tShape({ type: tNumber(updateTypes.UPDATE_THREAD), id: t.String, time: t.Number, - threadInfo: mixedRawThreadInfoValidator, + threadInfo: mixedThinRawThreadInfoValidator, }), getUpdatedThreadInfo(update: ThreadUpdateInfo) { return update.threadInfo; }, }); diff --git a/lib/types/minimally-encoded-thread-permissions-types.js b/lib/types/minimally-encoded-thread-permissions-types.js index cec45648f..137adaab2 100644 --- a/lib/types/minimally-encoded-thread-permissions-types.js +++ b/lib/types/minimally-encoded-thread-permissions-types.js @@ -1,294 +1,294 @@ // @flow import invariant from 'invariant'; import _mapValues from 'lodash/fp/mapValues.js'; import type { ClientAvatar } from './avatar-types.js'; import type { ThreadPermissionsInfo } from './thread-permission-types.js'; import type { ThreadType } from './thread-types-enum.js'; import type { ClientLegacyRoleInfo, LegacyMemberInfo, LegacyRawThreadInfo, LegacyThickRawThreadInfo, LegacyThinRawThreadInfo, LegacyThreadCurrentUserInfo, ThickMemberInfo, } from './thread-types.js'; import { decodeThreadRolePermissionsBitmaskArray, permissionsToBitmaskHex, threadPermissionsFromBitmaskHex, threadRolePermissionsBlobToBitmaskArray, } from '../permissions/minimally-encoded-thread-permissions.js'; import type { SpecialRole } from '../permissions/special-roles.js'; import { specialRoles } from '../permissions/special-roles.js'; import { roleIsAdminRole, roleIsDefaultRole } from '../shared/thread-utils.js'; import type { ThreadEntity } from '../utils/entity-text.js'; type RoleInfoBase = $ReadOnly<{ +id: string, +name: string, +minimallyEncoded: true, +permissions: $ReadOnlyArray, }>; export type RoleInfo = $ReadOnly<{ ...RoleInfoBase, +specialRole?: ?SpecialRole, }>; const minimallyEncodeRoleInfo = (roleInfo: ClientLegacyRoleInfo): RoleInfo => { invariant( !('minimallyEncoded' in roleInfo), 'roleInfo is already minimally encoded.', ); let specialRole: ?SpecialRole; if (roleIsDefaultRole(roleInfo)) { specialRole = specialRoles.DEFAULT_ROLE; } else if (roleIsAdminRole(roleInfo)) { specialRole = specialRoles.ADMIN_ROLE; } const { isDefault, ...rest } = roleInfo; return { ...rest, minimallyEncoded: true, permissions: threadRolePermissionsBlobToBitmaskArray(roleInfo.permissions), specialRole, }; }; const decodeMinimallyEncodedRoleInfo = ( minimallyEncodedRoleInfo: RoleInfo, ): ClientLegacyRoleInfo => { const { minimallyEncoded, specialRole, ...rest } = minimallyEncodedRoleInfo; return { ...rest, permissions: decodeThreadRolePermissionsBitmaskArray( minimallyEncodedRoleInfo.permissions, ), isDefault: roleIsDefaultRole(minimallyEncodedRoleInfo), }; }; export type ThreadCurrentUserInfo = $ReadOnly<{ ...LegacyThreadCurrentUserInfo, +minimallyEncoded: true, +permissions: string, }>; const minimallyEncodeThreadCurrentUserInfo = ( threadCurrentUserInfo: LegacyThreadCurrentUserInfo, ): ThreadCurrentUserInfo => { invariant( !('minimallyEncoded' in threadCurrentUserInfo), 'threadCurrentUserInfo is already minimally encoded.', ); return { ...threadCurrentUserInfo, minimallyEncoded: true, permissions: permissionsToBitmaskHex(threadCurrentUserInfo.permissions), }; }; const decodeMinimallyEncodedThreadCurrentUserInfo = ( minimallyEncodedThreadCurrentUserInfo: ThreadCurrentUserInfo, ): LegacyThreadCurrentUserInfo => { const { minimallyEncoded, ...rest } = minimallyEncodedThreadCurrentUserInfo; return { ...rest, permissions: threadPermissionsFromBitmaskHex( minimallyEncodedThreadCurrentUserInfo.permissions, ), }; }; export type MemberInfoWithPermissions = $ReadOnly<{ ...LegacyMemberInfo, +minimallyEncoded: true, +permissions: string, }>; export type MemberInfoSansPermissions = $Diff< MemberInfoWithPermissions, { +permissions: string }, >; export type MinimallyEncodedThickMemberInfo = $ReadOnly<{ ...ThickMemberInfo, +minimallyEncoded: true, +permissions: string, }>; const minimallyEncodeMemberInfo = ( memberInfo: T, ): $ReadOnly<{ ...T, +minimallyEncoded: true, +permissions: string, }> => { invariant( !('minimallyEncoded' in memberInfo), 'memberInfo is already minimally encoded.', ); return { ...memberInfo, minimallyEncoded: true, permissions: permissionsToBitmaskHex(memberInfo.permissions), }; }; const decodeMinimallyEncodedMemberInfo = < T: MemberInfoWithPermissions | MinimallyEncodedThickMemberInfo, >( minimallyEncodedMemberInfo: T, ): $ReadOnly<{ ...$Diff< T, { +minimallyEncoded: true, +permissions: string, }, >, +permissions: ThreadPermissionsInfo, }> => { const { minimallyEncoded, ...rest } = minimallyEncodedMemberInfo; return { ...rest, permissions: threadPermissionsFromBitmaskHex( minimallyEncodedMemberInfo.permissions, ), }; }; export type ThinRawThreadInfo = $ReadOnly<{ ...LegacyThinRawThreadInfo, +minimallyEncoded: true, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, }>; export type ThickRawThreadInfo = $ReadOnly<{ ...LegacyThickRawThreadInfo, +minimallyEncoded: true, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, }>; export type RawThreadInfo = ThinRawThreadInfo | ThickRawThreadInfo; const minimallyEncodeRawThreadInfoWithMemberPermissions = ( rawThreadInfo: LegacyRawThreadInfo, ): RawThreadInfo => { invariant( !('minimallyEncoded' in rawThreadInfo), 'rawThreadInfo is already minimally encoded.', ); if (rawThreadInfo.thick) { const { members, roles, currentUser, ...rest } = rawThreadInfo; return { ...rest, minimallyEncoded: true, members: members.map(minimallyEncodeMemberInfo), roles: _mapValues(minimallyEncodeRoleInfo)(roles), currentUser: minimallyEncodeThreadCurrentUserInfo(currentUser), }; } else { const { members, roles, currentUser, ...rest } = rawThreadInfo; // We removed the `.permissions` field from `MemberInfo`, but persisted // `MemberInfo`s will still have the field in legacy migrations. // $FlowIgnore return { ...rest, minimallyEncoded: true, members: members.map(minimallyEncodeMemberInfo), roles: _mapValues(minimallyEncodeRoleInfo)(roles), currentUser: minimallyEncodeThreadCurrentUserInfo(currentUser), }; } }; const deprecatedDecodeMinimallyEncodedRawThreadInfo = ( minimallyEncodedRawThreadInfo: RawThreadInfo, ): LegacyRawThreadInfo => { if (minimallyEncodedRawThreadInfo.thick) { const { minimallyEncoded, members, roles, currentUser, ...rest } = minimallyEncodedRawThreadInfo; return { ...rest, members: members.map(decodeMinimallyEncodedMemberInfo), roles: _mapValues(decodeMinimallyEncodedRoleInfo)(roles), currentUser: decodeMinimallyEncodedThreadCurrentUserInfo(currentUser), }; } else { const { minimallyEncoded, members, roles, currentUser, ...rest } = minimallyEncodedRawThreadInfo; return { ...rest, // We removed the `.permissions` field from `MemberInfo`, but persisted // `MemberInfo`s will still have the field in legacy migrations. // $FlowIgnore members: members.map(decodeMinimallyEncodedMemberInfo), roles: _mapValues(decodeMinimallyEncodedRoleInfo)(roles), currentUser: decodeMinimallyEncodedThreadCurrentUserInfo(currentUser), }; } }; export type RoleInfoWithoutSpecialRole = $ReadOnly<{ ...RoleInfoBase, +isDefault?: boolean, }>; -export type RawThreadInfoWithoutSpecialRole = $ReadOnly<{ - ...RawThreadInfo, +export type ThinRawThreadInfoWithoutSpecialRole = $ReadOnly<{ + ...ThinRawThreadInfo, +roles: { +[id: string]: RoleInfoWithoutSpecialRole }, }>; export type RelativeMemberInfo = { +id: string, +role: ?string, +isSender: boolean, +minimallyEncoded: true, +username: ?string, +isViewer: boolean, }; export type ThreadInfo = $ReadOnly<{ +minimallyEncoded: true, +id: string, +type: ThreadType, +name: ?string, +uiName: string | ThreadEntity, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }>; export type ResolvedThreadInfo = $ReadOnly<{ ...ThreadInfo, +uiName: string, }>; export { minimallyEncodeRoleInfo, decodeMinimallyEncodedRoleInfo, minimallyEncodeThreadCurrentUserInfo, decodeMinimallyEncodedThreadCurrentUserInfo, minimallyEncodeMemberInfo, decodeMinimallyEncodedMemberInfo, minimallyEncodeRawThreadInfoWithMemberPermissions, deprecatedDecodeMinimallyEncodedRawThreadInfo, }; diff --git a/lib/types/request-types.js b/lib/types/request-types.js index eaaa3ef81..99ae09e30 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,337 +1,337 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate, activityUpdateValidator, } from './activity-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { signedIdentityKeysBlobValidator } from './crypto-types.js'; import type { Platform, PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery, rawEntryInfoValidator, } from './entry-types.js'; import type { RawThreadInfo } from './minimally-encoded-thread-permissions-types'; import type { DispatchMetadata } from './redux-types.js'; import { type ThreadInconsistencyReportShape, type EntryInconsistencyReportShape, type ClientThreadInconsistencyReportShape, type ClientEntryInconsistencyReportShape, threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, } from './report-types.js'; import type { LegacyRawThreadInfo } from './thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type AccountUserInfo, accountUserInfoValidator, } from './user-types.js'; -import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tNumber, tShape, tID, tUserID, tPlatform, tPlatformDetails, } from '../utils/validation-utils.js'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, // MORE_ONE_TIME_KEYS: 8, (DEPRECATED) SIGNED_IDENTITY_KEYS_BLOB: 9, INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE: 10, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || serverRequestType === 9 || serverRequestType === 10, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; const platformServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM), }); type PlatformClientResponse = { +type: 0, +platform: Platform, }; const platformClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.PLATFORM), platform: tPlatform, }); export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; const threadInconsistencyClientResponseValidator: TInterface = tShape({ ...threadInconsistencyReportValidatorShape, type: tNumber(serverRequestTypes.THREAD_INCONSISTENCY), }); type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; const platformDetailsClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), platformDetails: tPlatformDetails, }); export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; const entryInconsistencyClientResponseValidator: TInterface = tShape({ ...entryInconsistencyReportValidatorShape, type: tNumber(serverRequestTypes.ENTRY_INCONSISTENCY), }); type FailUnmentioned = Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>; type StateChanges = Partial<{ +rawThreadInfos: LegacyRawThreadInfo[] | RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: FailUnmentioned, +stateChanges?: StateChanges, }; const serverCheckStateServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashesToCheck: t.dict(t.String, t.Number), failUnmentioned: t.maybe( tShape({ threadInfos: t.maybe(t.Boolean), entryInfos: t.maybe(t.Boolean), userInfos: t.maybe(t.Boolean), }), ), stateChanges: t.maybe( tShape({ - rawThreadInfos: t.maybe(t.list(mixedRawThreadInfoValidator)), + rawThreadInfos: t.maybe(t.list(mixedThinRawThreadInfoValidator)), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), currentUserInfo: t.maybe(currentUserInfoValidator), userInfos: t.maybe(t.list(accountUserInfoValidator)), deleteThreadIDs: t.maybe(t.list(tID)), deleteEntryIDs: t.maybe(t.list(tID)), deleteUserInfoIDs: t.maybe(t.list(tUserID)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; const checkStateClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashResults: t.dict(t.String, t.Boolean), }); type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; const initialActivityUpdatesClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.INITIAL_ACTIVITY_UPDATES), activityUpdates: t.list(activityUpdateValidator), }); type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; type SignedIdentityKeysBlobServerRequest = { +type: 9, }; const signedIdentityKeysBlobServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), }); type SignedIdentityKeysBlobClientResponse = { +type: 9, +signedIdentityKeysBlob: SignedIdentityKeysBlob, }; const signedIdentityKeysBlobClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), signedIdentityKeysBlob: signedIdentityKeysBlobValidator, }); type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: string, }; const initialNotificationsEncryptedMessageClientResponseValidator: TInterface = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), initialNotificationsEncryptedMessage: t.String, }); export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; export const serverServerRequestValidator: TUnion = t.union([ platformServerRequestValidator, platformDetailsServerRequestValidator, serverCheckStateServerRequestValidator, signedIdentityKeysBlobServerRequestValidator, initialNotificationsEncryptedMessageServerRequestValidator, ]); export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Partial<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, +keyserverID: string, }; export type ProcessServerRequestAction = { +dispatchMetadata?: DispatchMetadata, +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, }; export const clientResponseInputValidator: TUnion = t.union([ platformClientResponseValidator, threadInconsistencyClientResponseValidator, entryInconsistencyClientResponseValidator, platformDetailsClientResponseValidator, checkStateClientResponseValidator, initialActivityUpdatesClientResponseValidator, signedIdentityKeysBlobClientResponseValidator, initialNotificationsEncryptedMessageClientResponseValidator, ]); diff --git a/lib/types/socket-types.js b/lib/types/socket-types.js index faa36e12b..fe0bc9acd 100644 --- a/lib/types/socket-types.js +++ b/lib/types/socket-types.js @@ -1,564 +1,564 @@ // @flow import invariant from 'invariant'; import t, { type TInterface, type TUnion } from 'tcomb'; import { type RecoveryFromReduxActionSource, recoveryFromReduxActionSources, } from './account-types.js'; import { type ActivityUpdate, activityUpdateValidator, type UpdateActivityResult, updateActivityResultValidator, } from './activity-types.js'; import { type CompressedData, compressedDataValidator, } from './compression-types.js'; import type { APIRequest } from './endpoints.js'; import { type RawEntryInfo, rawEntryInfoValidator, type CalendarQuery, } from './entry-types.js'; import { type MessagesResponse, messagesResponseValidator, type NewMessagesPayload, newMessagesPayloadValidator, } from './message-types.js'; import { type ServerServerRequest, serverServerRequestValidator, type ClientServerRequest, type ClientResponse, type ClientClientResponse, } from './request-types.js'; import type { SessionState, SessionIdentification } from './session-types.js'; import type { MixedRawThreadInfos, RawThreadInfos } from './thread-types.js'; import { type ClientUpdatesResult, type ClientUpdatesResultWithUserInfos, type ServerUpdatesResult, serverUpdatesResultValidator, type ServerUpdatesResultWithUserInfos, serverUpdatesResultWithUserInfosValidator, } from './update-types.js'; import { type UserInfo, userInfoValidator, type CurrentUserInfo, currentUserInfoValidator, type LoggedOutUserInfo, loggedOutUserInfoValidator, } from './user-types.js'; -import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import { values } from '../utils/objects.js'; import { tShape, tNumber, tID } from '../utils/validation-utils.js'; // The types of messages that the client sends across the socket export const clientSocketMessageTypes = Object.freeze({ INITIAL: 0, RESPONSES: 1, //ACTIVITY_UPDATES: 2, (DEPRECATED) PING: 3, ACK_UPDATES: 4, API_REQUEST: 5, }); export type ClientSocketMessageType = $Values; export function assertClientSocketMessageType( ourClientSocketMessageType: number, ): ClientSocketMessageType { invariant( ourClientSocketMessageType === 0 || ourClientSocketMessageType === 1 || ourClientSocketMessageType === 3 || ourClientSocketMessageType === 4 || ourClientSocketMessageType === 5, 'number is not ClientSocketMessageType enum', ); return ourClientSocketMessageType; } export type InitialClientSocketMessage = { +type: 0, +id: number, +payload: { +sessionIdentification: SessionIdentification, +sessionState: SessionState, +clientResponses: $ReadOnlyArray, }, }; export type ResponsesClientSocketMessage = { +type: 1, +id: number, +payload: { +clientResponses: $ReadOnlyArray, }, }; export type PingClientSocketMessage = { +type: 3, +id: number, }; export type AckUpdatesClientSocketMessage = { +type: 4, +id: number, +payload: { +currentAsOf: number, }, }; export type APIRequestClientSocketMessage = { +type: 5, +id: number, +payload: APIRequest, }; export type ClientSocketMessage = | InitialClientSocketMessage | ResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientInitialClientSocketMessage = { +type: 0, +id: number, +payload: { +sessionIdentification: SessionIdentification, +sessionState: SessionState, +clientResponses: $ReadOnlyArray, }, }; export type ClientResponsesClientSocketMessage = { +type: 1, +id: number, +payload: { +clientResponses: $ReadOnlyArray, }, }; export type ClientClientSocketMessage = | ClientInitialClientSocketMessage | ClientResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientSocketMessageWithoutID = $Diff< ClientClientSocketMessage, { id: number }, >; // The types of messages that the server sends across the socket export const serverSocketMessageTypes = Object.freeze({ STATE_SYNC: 0, REQUESTS: 1, ERROR: 2, AUTH_ERROR: 3, ACTIVITY_UPDATE_RESPONSE: 4, PONG: 5, UPDATES: 6, MESSAGES: 7, API_RESPONSE: 8, COMPRESSED_MESSAGE: 9, }); export type ServerSocketMessageType = $Values; export function assertServerSocketMessageType( ourServerSocketMessageType: number, ): ServerSocketMessageType { invariant( ourServerSocketMessageType === 0 || ourServerSocketMessageType === 1 || ourServerSocketMessageType === 2 || ourServerSocketMessageType === 3 || ourServerSocketMessageType === 4 || ourServerSocketMessageType === 5 || ourServerSocketMessageType === 6 || ourServerSocketMessageType === 7 || ourServerSocketMessageType === 8 || ourServerSocketMessageType === 9, 'number is not ServerSocketMessageType enum', ); return ourServerSocketMessageType; } export const stateSyncPayloadTypes = Object.freeze({ FULL: 0, INCREMENTAL: 1, }); export const fullStateSyncActionType = 'FULL_STATE_SYNC'; export type BaseFullStateSync = { +messagesResult: MessagesResponse, +rawEntryInfos: $ReadOnlyArray, +userInfos: $ReadOnlyArray, +updatesCurrentAsOf: number, }; const baseFullStateSyncValidator = tShape({ messagesResult: messagesResponseValidator, rawEntryInfos: t.list(rawEntryInfoValidator), userInfos: t.list(userInfoValidator), updatesCurrentAsOf: t.Number, }); export type ClientFullStateSync = $ReadOnly<{ ...BaseFullStateSync, +threadInfos: RawThreadInfos, +currentUserInfo: CurrentUserInfo, }>; export type StateSyncFullActionPayload = $ReadOnly<{ ...ClientFullStateSync, +calendarQuery: CalendarQuery, +keyserverID: string, }>; export type ClientStateSyncFullSocketPayload = $ReadOnly<{ ...ClientFullStateSync, +type: 0, // Included iff client is using sessionIdentifierTypes.BODY_SESSION_ID +sessionID?: string, }>; export type ServerFullStateSync = $ReadOnly<{ ...BaseFullStateSync, +threadInfos: MixedRawThreadInfos, +currentUserInfo: CurrentUserInfo, }>; const serverFullStateSyncValidator = tShape({ ...baseFullStateSyncValidator.meta.props, - threadInfos: t.dict(tID, mixedRawThreadInfoValidator), + threadInfos: t.dict(tID, mixedThinRawThreadInfoValidator), currentUserInfo: currentUserInfoValidator, }); export type ServerStateSyncFullSocketPayload = $ReadOnly<{ ...ServerFullStateSync, +type: 0, // Included iff client is using sessionIdentifierTypes.BODY_SESSION_ID +sessionID?: string, }>; const serverStateSyncFullSocketPayloadValidator = tShape({ ...serverFullStateSyncValidator.meta.props, type: tNumber(stateSyncPayloadTypes.FULL), sessionID: t.maybe(t.String), }); export const incrementalStateSyncActionType = 'INCREMENTAL_STATE_SYNC'; export type BaseIncrementalStateSync = { +messagesResult: MessagesResponse, +deltaEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; const baseIncrementalStateSyncValidator = tShape({ messagesResult: messagesResponseValidator, deltaEntryInfos: t.list(rawEntryInfoValidator), deletedEntryIDs: t.list(tID), userInfos: t.list(userInfoValidator), }); export type ClientIncrementalStateSync = $ReadOnly<{ ...BaseIncrementalStateSync, +updatesResult: ClientUpdatesResult, }>; export type StateSyncIncrementalActionPayload = $ReadOnly<{ ...ClientIncrementalStateSync, +calendarQuery: CalendarQuery, +keyserverID: string, }>; export type ClientStateSyncIncrementalSocketPayload = $ReadOnly<{ +type: 1, ...ClientIncrementalStateSync, }>; export type ServerIncrementalStateSync = $ReadOnly<{ ...BaseIncrementalStateSync, +updatesResult: ServerUpdatesResult, }>; const serverIncrementalStateSyncValidator = tShape({ ...baseIncrementalStateSyncValidator.meta.props, updatesResult: serverUpdatesResultValidator, }); type ServerStateSyncIncrementalSocketPayload = $ReadOnly<{ +type: 1, ...ServerIncrementalStateSync, }>; const serverStateSyncIncrementalSocketPayloadValidator = tShape({ type: tNumber(stateSyncPayloadTypes.INCREMENTAL), ...serverIncrementalStateSyncValidator.meta.props, }); export type ClientStateSyncSocketPayload = | ClientStateSyncFullSocketPayload | ClientStateSyncIncrementalSocketPayload; export type ServerStateSyncSocketPayload = | ServerStateSyncFullSocketPayload | ServerStateSyncIncrementalSocketPayload; export const serverStateSyncSocketPayloadValidator: TUnion = t.union([ serverStateSyncFullSocketPayloadValidator, serverStateSyncIncrementalSocketPayloadValidator, ]); export type ClientStateSyncFullSocketResult = $ReadOnly<{ ...ClientStateSyncFullSocketPayload, +keyserverID: string, }>; export type ClientStateSyncIncrementalSocketResult = $ReadOnly<{ ...ClientStateSyncIncrementalSocketPayload, +keyserverID: string, }>; export type ClientStateSyncSocketResult = | ClientStateSyncFullSocketResult | ClientStateSyncIncrementalSocketResult; export type ServerStateSyncServerSocketMessage = { +type: 0, +responseTo: number, +payload: ServerStateSyncSocketPayload, }; export const serverStateSyncServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.STATE_SYNC), responseTo: t.Number, payload: serverStateSyncSocketPayloadValidator, }); type ServerRequestsServerSocketMessagePayload = { +serverRequests: $ReadOnlyArray, }; export type ServerRequestsServerSocketMessage = { +type: 1, +responseTo?: number, +payload: ServerRequestsServerSocketMessagePayload, }; export const serverRequestsServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.REQUESTS), responseTo: t.maybe(t.Number), payload: tShape({ serverRequests: t.list(serverServerRequestValidator), }), }); export type ErrorServerSocketMessage = { type: 2, responseTo?: number, message: string, payload?: Object, }; export const errorServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.ERROR), responseTo: t.maybe(t.Number), message: t.String, payload: t.maybe(t.Object), }); type SessionChange = { +cookie: string, +currentUserInfo: LoggedOutUserInfo, }; export type AuthErrorServerSocketMessage = { +type: 3, +responseTo: number, +message: string, +sessionChange: SessionChange, }; export const authErrorServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.AUTH_ERROR), responseTo: t.Number, message: t.String, sessionChange: t.maybe( tShape({ cookie: t.String, currentUserInfo: loggedOutUserInfoValidator, }), ), }); export type ActivityUpdateResponseServerSocketMessage = { +type: 4, +responseTo: number, +payload: UpdateActivityResult, }; export const activityUpdateResponseServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE), responseTo: t.Number, payload: updateActivityResultValidator, }); export type PongServerSocketMessage = { +type: 5, +responseTo: number, }; export const pongServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.PONG), responseTo: t.Number, }); export type ServerUpdatesServerSocketMessage = { +type: 6, +payload: ServerUpdatesResultWithUserInfos, }; export const serverUpdatesServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.UPDATES), payload: serverUpdatesResultWithUserInfosValidator, }); export type MessagesServerSocketMessage = { +type: 7, +payload: NewMessagesPayload, }; export const messagesServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.MESSAGES), payload: newMessagesPayloadValidator, }); export type APIResponseServerSocketMessage = { +type: 8, +responseTo: number, +payload?: Object, }; export const apiResponseServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.API_RESPONSE), responseTo: t.Number, payload: t.maybe(t.Object), }); export type CompressedMessageServerSocketMessage = { +type: 9, +payload: CompressedData, }; export const compressedMessageServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.COMPRESSED_MESSAGE), payload: compressedDataValidator, }); export type ServerServerSocketMessage = | ServerStateSyncServerSocketMessage | ServerRequestsServerSocketMessage | ErrorServerSocketMessage | AuthErrorServerSocketMessage | ActivityUpdateResponseServerSocketMessage | PongServerSocketMessage | ServerUpdatesServerSocketMessage | MessagesServerSocketMessage | APIResponseServerSocketMessage | CompressedMessageServerSocketMessage; export const serverServerSocketMessageValidator: TUnion = t.union([ serverStateSyncServerSocketMessageValidator, serverRequestsServerSocketMessageValidator, errorServerSocketMessageValidator, authErrorServerSocketMessageValidator, activityUpdateResponseServerSocketMessageValidator, pongServerSocketMessageValidator, serverUpdatesServerSocketMessageValidator, messagesServerSocketMessageValidator, apiResponseServerSocketMessageValidator, compressedMessageServerSocketMessageValidator, ]); export type ClientRequestsServerSocketMessage = { +type: 1, +responseTo?: number, +payload: { +serverRequests: $ReadOnlyArray, }, }; export type ClientStateSyncServerSocketMessage = { +type: 0, +responseTo: number, +payload: ClientStateSyncSocketPayload, }; export type ClientUpdatesServerSocketMessage = { +type: 6, +payload: ClientUpdatesResultWithUserInfos, }; export type ClientServerSocketMessage = | ClientStateSyncServerSocketMessage | ClientRequestsServerSocketMessage | ErrorServerSocketMessage | AuthErrorServerSocketMessage | ActivityUpdateResponseServerSocketMessage | PongServerSocketMessage | ClientUpdatesServerSocketMessage | MessagesServerSocketMessage | APIResponseServerSocketMessage | CompressedMessageServerSocketMessage; export type SocketListener = (message: ClientServerSocketMessage) => void; export type ConnectionStatus = | 'connecting' | 'connected' | 'reconnecting' | 'disconnecting' | 'forcedDisconnecting' | 'disconnected'; export type ConnectionIssue = 'client_version_unsupported'; export type ConnectionInfo = { +status: ConnectionStatus, +queuedActivityUpdates: $ReadOnlyArray, +lateResponses: $ReadOnlyArray, +unreachable: boolean, +connectionIssue: ?ConnectionIssue, // When this is flipped to truthy, a session recovery is attempted // This can happen when the keyserver invalidates the session +activeSessionRecovery: null | RecoveryFromReduxActionSource, }; export const connectionInfoValidator: TInterface = tShape({ status: t.enums.of([ 'connecting', 'connected', 'reconnecting', 'disconnecting', 'forcedDisconnecting', 'disconnected', ]), queuedActivityUpdates: t.list(activityUpdateValidator), lateResponses: t.list(t.Number), unreachable: t.Boolean, connectionIssue: t.maybe(t.enums.of([])), activeSessionRecovery: t.maybe( t.enums.of(values(recoveryFromReduxActionSources)), ), }); export const defaultConnectionInfo: ConnectionInfo = { status: 'connecting', queuedActivityUpdates: [], lateResponses: [], unreachable: false, connectionIssue: null, activeSessionRecovery: null, }; export type SetActiveSessionRecoveryPayload = { +activeSessionRecovery: null | RecoveryFromReduxActionSource, +keyserverID: string, }; export type OneTimeKeyGenerator = (inc: number) => string; export type GRPCStream = { readyState: number, onopen: (ev: any) => mixed, onmessage: (ev: MessageEvent) => mixed, onclose: (ev: CloseEvent) => mixed, close(code?: number, reason?: string): void, send(data: string | Blob | ArrayBuffer | $ArrayBufferView): void, }; export type CommTransportLayer = GRPCStream | WebSocket; diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js index 6ead691b5..72d7ab09d 100644 --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -1,197 +1,201 @@ // @flow import invariant from 'invariant'; import type { TRefinement } from 'tcomb'; import { values } from '../utils/objects.js'; import { tNumEnum } from '../utils/validation-utils.js'; // Should be in sync with native/cpp/CommonCpp/NativeModules/\ // PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h export const thinThreadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent SIDEBAR: 5, // canonical thread for each pair of users. represents the friendship // created under GENESIS. being deprecated in favor of PERSONAL GENESIS_PERSONAL: 6, // canonical thread for each single user // created under GENESIS. being deprecated in favor of PRIVATE GENESIS_PRIVATE: 7, // aka "org". no parent, top-level, has admin COMMUNITY_ROOT: 8, // like COMMUNITY_ROOT, but members aren't voiced COMMUNITY_ANNOUNCEMENT_ROOT: 9, // an open subthread. has parent, top-level (not sidebar), and visible to all // members of parent. root ancestor is a COMMUNITY_ROOT COMMUNITY_OPEN_SUBTHREAD: 3, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD: 10, // a secret subthread. optional parent, top-level (not sidebar), visible only // to its members. root ancestor is a COMMUNITY_ROOT COMMUNITY_SECRET_SUBTHREAD: 4, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD: 11, // like COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, but you can't leave GENESIS: 12, }); export type ThinThreadType = $Values; export const nonSidebarThickThreadTypes = Object.freeze({ // local "thick" thread (outside of community). no parent, can only have // sidebar children LOCAL: 13, // canonical thread for each pair of users. represents the friendship PERSONAL: 14, // canonical thread for each single user PRIVATE: 15, }); export type NonSidebarThickThreadType = $Values< typeof nonSidebarThickThreadTypes, >; export const sidebarThickThreadTypes = Object.freeze({ // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent THICK_SIDEBAR: 16, }); export type SidebarThickThreadType = $Values; export const thickThreadTypes = Object.freeze({ ...nonSidebarThickThreadTypes, ...sidebarThickThreadTypes, }); export type ThickThreadType = | NonSidebarThickThreadType | SidebarThickThreadType; export type ThreadType = ThinThreadType | ThickThreadType; export const threadTypes = Object.freeze({ ...thinThreadTypes, ...thickThreadTypes, }); const thickThreadTypesSet = new Set(Object.values(thickThreadTypes)); export function threadTypeIsThick(threadType: ThreadType): boolean { return thickThreadTypesSet.has(threadType); } export function assertThinThreadType(threadType: number): ThinThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12, 'number is not ThinThreadType enum', ); return threadType; } +export const thinThreadTypeValidator: TRefinement = tNumEnum( + values(thinThreadTypes), +); + export function assertThickThreadType(threadType: number): ThickThreadType { invariant( threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThickThreadType enum', ); return threadType; } export const thickThreadTypeValidator: TRefinement = tNumEnum( values(thickThreadTypes), ); export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12 || threadType === 13 || threadType === 14 || threadType === 15 || threadType === 16, 'number is not ThreadType enum', ); return threadType; } export const threadTypeValidator: TRefinement = tNumEnum( values(threadTypes), ); export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.GENESIS, ]); export const announcementThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.GENESIS, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export const communitySubthreads: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export const sidebarThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.SIDEBAR, threadTypes.THICK_SIDEBAR, ]); export const personalThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.PERSONAL, threadTypes.GENESIS_PERSONAL, ]); export const privateThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.PRIVATE, threadTypes.GENESIS_PRIVATE, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export function threadTypeIsAnnouncementThread( threadType: ThreadType, ): boolean { return announcementThreadTypes.includes(threadType); } export function threadTypeIsSidebar(threadType: ThreadType): boolean { return sidebarThreadTypes.includes(threadType); } export function threadTypeIsPersonal(threadType: ThreadType): boolean { return personalThreadTypes.includes(threadType); } export function threadTypeIsPrivate(threadType: ThreadType): boolean { return privateThreadTypes.includes(threadType); } diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index daede9422..93dfee35f 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,536 +1,536 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type AvatarDBContent, type ClientAvatar, clientAvatarValidator, type UpdateUserAvatarRequest, } from './avatar-types.js'; import type { CalendarQuery } from './entry-types.js'; import type { Media } from './media-types.js'; import type { MessageTruncationStatuses, RawMessageInfo, } from './message-types.js'; import type { RawThreadInfo, ResolvedThreadInfo, ThreadInfo, ThickRawThreadInfo, } from './minimally-encoded-thread-permissions-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type ThreadPermissionsInfo, threadPermissionsInfoValidator, type ThreadRolePermissionsBlob, threadRolePermissionsBlobValidator, type UserSurfacedPermission, } from './thread-permission-types.js'; import { type ThinThreadType, type ThickThreadType, - threadTypeValidator, + thinThreadTypeValidator, } from './thread-types-enum.js'; import type { ClientUpdateInfo, ServerUpdateInfo } from './update-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import type { SpecialRole } from '../permissions/special-roles.js'; import { type ThreadEntity } from '../utils/entity-text.js'; import { tID, tShape, tUserID } from '../utils/validation-utils.js'; export type LegacyMemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +isSender: boolean, }; export const legacyMemberInfoValidator: TInterface = tShape({ id: tUserID, role: t.maybe(tID), permissions: threadPermissionsInfoValidator, isSender: t.Boolean, }); export type ClientLegacyRoleInfo = { +id: string, +name: string, +permissions: ThreadRolePermissionsBlob, +isDefault: boolean, }; export const clientLegacyRoleInfoValidator: TInterface = tShape({ id: tID, name: t.String, permissions: threadRolePermissionsBlobValidator, isDefault: t.Boolean, }); export type ServerLegacyRoleInfo = { +id: string, +name: string, +permissions: ThreadRolePermissionsBlob, +isDefault: boolean, +specialRole: ?SpecialRole, }; export type LegacyThreadCurrentUserInfo = { +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, }; export const legacyThreadCurrentUserInfoValidator: TInterface = tShape({ role: t.maybe(tID), permissions: threadPermissionsInfoValidator, subscription: threadSubscriptionValidator, unread: t.maybe(t.Boolean), }); export type LegacyThinRawThreadInfo = { +id: string, +type: ThinThreadType, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID?: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: ClientLegacyRoleInfo }, +currentUser: LegacyThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ThickMemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +isSender: boolean, }; export type ThreadTimestamps = { +name: number, +avatar: number, +description: number, +color: number, +members: { +[id: string]: { +isMember: number, +subscription: number, }, }, +currentUser: { +unread: number, }, }; export const threadTimestampsValidator: TInterface = tShape({ name: t.Number, avatar: t.Number, description: t.Number, color: t.Number, members: t.dict( tUserID, tShape({ isMember: t.Number, subscription: t.Number, }), ), currentUser: tShape({ unread: t.Number, }), }); export type LegacyThickRawThreadInfo = { +thick: true, +id: string, +type: ThickThreadType, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID?: ?string, +containingThreadID?: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: ClientLegacyRoleInfo }, +currentUser: LegacyThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, +timestamps: ThreadTimestamps, }; export type LegacyRawThreadInfo = | LegacyThinRawThreadInfo | LegacyThickRawThreadInfo; export type LegacyRawThreadInfos = { +[id: string]: LegacyRawThreadInfo, }; -export const legacyRawThreadInfoValidator: TInterface = - tShape({ +export const legacyThinRawThreadInfoValidator: TInterface = + tShape({ id: tID, - type: threadTypeValidator, + type: thinThreadTypeValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.String, creationTime: t.Number, parentThreadID: t.maybe(tID), containingThreadID: t.maybe(tID), community: t.maybe(tID), members: t.list(legacyMemberInfoValidator), roles: t.dict(tID, clientLegacyRoleInfoValidator), currentUser: legacyThreadCurrentUserInfoValidator, sourceMessageID: t.maybe(tID), repliesCount: t.Number, pinnedCount: t.maybe(t.Number), }); export type MixedRawThreadInfos = { +[id: string]: LegacyRawThreadInfo | RawThreadInfo, }; export type ThickRawThreadInfos = { +[id: string]: ThickRawThreadInfo, }; export type RawThreadInfos = { +[id: string]: RawThreadInfo, }; export type ServerMemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, +isSender: boolean, }; export type ServerThreadInfo = { +id: string, +type: ThinThreadType, +name: ?string, +avatar?: AvatarDBContent, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +depth: number, +members: $ReadOnlyArray, +roles: { +[id: string]: ServerLegacyRoleInfo }, +sourceMessageID?: string, +repliesCount: number, +pinnedCount: number, }; export type LegacyThreadStore = { +threadInfos: MixedRawThreadInfos, }; export type ThreadStore = { +threadInfos: RawThreadInfos, }; export type ClientDBThreadInfo = { +id: string, +type: number, +name: ?string, +avatar?: ?string, +description: ?string, +color: string, +creationTime: string, +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: string, +roles: string, +currentUser: string, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, +timestamps?: ?string, }; export type ThreadDeletionRequest = { +threadID: string, +accountPassword?: empty, }; export type RemoveMembersRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, }; export type RoleChangeRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, +role: string, }; export type ChangeThreadSettingsResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type ChangeThreadSettingsPayload = { +threadID: string, +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type LeaveThreadRequest = { +threadID: string, }; export type LeaveThreadResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type LeaveThreadPayload = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; type BaseThreadChanges = { +type: ThinThreadType, +name: string, +description: string, +color: string, +parentThreadID: ?string, +avatar: UpdateUserAvatarRequest, }; export type ThreadChanges = Partial; export type ThinThreadChanges = $ReadOnly< $Partial<{ ...BaseThreadChanges, +newMemberIDs: $ReadOnlyArray }>, >; export type UpdateThreadRequest = { +threadID: string, +changes: ThinThreadChanges, +accountPassword?: empty, }; export type UpdateThickThreadRequest = $ReadOnly<{ ...UpdateThreadRequest, +changes: ThreadChanges, }>; export type BaseNewThreadRequest = { +id?: ?string, +name?: ?string, +description?: ?string, +color?: ?string, +parentThreadID?: ?string, +initialMemberIDs?: ?$ReadOnlyArray, +ghostMemberIDs?: ?$ReadOnlyArray, }; type NewThinThreadRequest = | $ReadOnly<{ +type: 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12, ...BaseNewThreadRequest, }> | $ReadOnly<{ +type: 5, +sourceMessageID: string, ...BaseNewThreadRequest, +parentThreadID: string, }>; export type ClientNewThinThreadRequest = $ReadOnly<{ ...NewThinThreadRequest, +calendarQuery: CalendarQuery, }>; export type ServerNewThinThreadRequest = $ReadOnly<{ ...NewThinThreadRequest, +calendarQuery?: ?CalendarQuery, }>; export type NewThickThreadRequest = | $ReadOnly<{ +type: 13 | 14 | 15, ...BaseNewThreadRequest, }> | $ReadOnly<{ +type: 16, +sourceMessageID: string, ...BaseNewThreadRequest, +parentThreadID: string, }>; export type NewThreadResponse = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +userInfos: UserInfos, +newThreadID: string, }; export type NewThreadResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +userInfos: UserInfos, +newThreadID: string, }; export type ServerThreadJoinRequest = { +threadID: string, +calendarQuery?: ?CalendarQuery, +inviteLinkSecret?: string, +defaultSubscription?: ThreadSubscription, }; export type ClientThreadJoinRequest = { +threadID: string, +calendarQuery: CalendarQuery, +inviteLinkSecret?: string, +defaultSubscription?: ThreadSubscription, }; export type ThreadJoinResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: UserInfos, }; export type ThreadJoinPayload = { +updatesResult: { newUpdates: $ReadOnlyArray, }, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +keyserverID?: string, }; export type ThreadFetchMediaResult = { +media: $ReadOnlyArray, }; export type ThreadFetchMediaRequest = { +threadID: string, +limit: number, +offset: number, }; export type LastUpdatedTimes = { // The last updated time is at least this number, but possibly higher // We won't know for sure until the below Promise resolves +lastUpdatedAtLeastTime: number, +lastUpdatedTime: Promise, }; export type SidebarInfo = $ReadOnly<{ ...LastUpdatedTimes, +threadInfo: ThreadInfo, +mostRecentNonLocalMessage: ?string, }>; export type ToggleMessagePinRequest = { +messageID: string, +action: 'pin' | 'unpin', }; export type ToggleMessagePinResult = { +newMessageInfos: $ReadOnlyArray, +threadID: string, }; type CreateRoleAction = { +community: string, +name: string, +permissions: $ReadOnlyArray, +action: 'create_role', }; type EditRoleAction = { +community: string, +existingRoleID: string, +name: string, +permissions: $ReadOnlyArray, +action: 'edit_role', }; export type RoleModificationRequest = CreateRoleAction | EditRoleAction; export type RoleModificationResult = { +threadInfo: LegacyRawThreadInfo | RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type RoleModificationPayload = { +threadInfo: LegacyRawThreadInfo | RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type RoleDeletionRequest = { +community: string, +roleID: string, }; export type RoleDeletionResult = { +threadInfo: LegacyRawThreadInfo | RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type RoleDeletionPayload = { +threadInfo: LegacyRawThreadInfo | RawThreadInfo, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; // We can show a max of 3 sidebars inline underneath their parent in the chat // tab. If there are more, we show a button that opens a modal to see the rest export const maxReadSidebars = 3; // We can show a max of 5 sidebars inline underneath their parent // in the chat tab if every one of the displayed sidebars is unread export const maxUnreadSidebars = 5; export type ThreadStoreThreadInfos = LegacyRawThreadInfos; export type ChatMentionCandidate = { +threadInfo: ResolvedThreadInfo, +rawChatName: string | ThreadEntity, }; export type ChatMentionCandidates = { +[id: string]: ChatMentionCandidate, }; export type ChatMentionCandidatesObj = { +[id: string]: ChatMentionCandidates, }; export type UserProfileThreadInfo = { +threadInfo: ThreadInfo, +pendingPersonalThreadUserInfo?: UserInfo, }; diff --git a/lib/types/validation.test.js b/lib/types/validation.test.js index 4306aa837..30a9608ee 100644 --- a/lib/types/validation.test.js +++ b/lib/types/validation.test.js @@ -1,874 +1,877 @@ // @flow import _findKey from 'lodash/fp/findKey.js'; import { rawEntryInfoValidator } from './entry-types.js'; import { imageValidator, videoValidator, mediaValidator, } from './media-types.js'; import { messageTypes } from './message-types-enum.js'; import { activityUpdateResponseServerSocketMessageValidator, apiResponseServerSocketMessageValidator, authErrorServerSocketMessageValidator, errorServerSocketMessageValidator, messagesServerSocketMessageValidator, pongServerSocketMessageValidator, serverRequestsServerSocketMessageValidator, serverSocketMessageTypes, serverStateSyncServerSocketMessageValidator, serverUpdatesServerSocketMessageValidator, } from './socket-types.js'; import { threadTypes } from './thread-types-enum.js'; -import { legacyRawThreadInfoValidator } from './thread-types.js'; +import { legacyThinRawThreadInfoValidator } from './thread-types.js'; import { updateTypes } from './update-types-enum.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import { values } from '../utils/objects.js'; describe('media validation', () => { const photo = { id: '92696', type: 'photo', uri: 'http://0.0.0.0:3000/comm/upload/92696/0fb272bd1c75d976', dimensions: { width: 340, height: 288, }, }; const video = { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }; it('should validate correct media', () => { expect(mediaValidator.is(photo)).toBe(true); expect(imageValidator.is(photo)).toBe(true); expect(mediaValidator.is(video)).toBe(true); expect(videoValidator.is(video)).toBe(true); }); it('should not validate incorrect media', () => { expect(imageValidator.is(video)).toBe(false); expect(videoValidator.is(photo)).toBe(false); expect(mediaValidator.is({ ...photo, type: undefined })).toBe(false); expect(mediaValidator.is({ ...video, dimensions: undefined })).toBe(false); }); }); const messages = [ { type: messageTypes.TEXT, threadID: '83859', creatorID: '83853', time: 1682077048858, text: 'text', localID: 'local1', id: '92837', }, { type: messageTypes.CREATE_THREAD, id: '83876', threadID: '83859', time: 1673561105839, creatorID: '83853', initialThreadState: { type: 6, name: null, parentThreadID: '1', color: '57697f', memberIDs: ['256', '83853'], }, }, { type: messageTypes.ADD_MEMBERS, id: '4754380', threadID: '4746046', time: 1680179819346, creatorID: '256', addedUserIDs: ['518252', '1329299', '1559042'], }, { type: messageTypes.CREATE_SUB_THREAD, threadID: '87111', creatorID: '83928', time: 1682083573756, childThreadID: '92993', id: '93000', }, { type: messageTypes.CHANGE_SETTINGS, threadID: '83859', creatorID: '83853', time: 1682082984605, field: 'color', value: 'b8753d', id: '92880', }, { type: messageTypes.REMOVE_MEMBERS, threadID: '92993', creatorID: '83928', time: 1682083613415, removedUserIDs: ['83890'], id: '93012', }, { type: messageTypes.CHANGE_ROLE, threadID: '85027', creatorID: '256', time: 1632393331694, userIDs: ['85081'], newRole: 'role', id: '85431', }, { type: messageTypes.LEAVE_THREAD, id: '93027', threadID: '92993', time: 1682083651037, creatorID: '83928', }, { type: messageTypes.JOIN_THREAD, threadID: '92993', creatorID: '83928', time: 1682083678595, id: '93035', }, { type: messageTypes.CREATE_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083217395, entryID: '92917', date: '2023-04-02', text: 'text', id: '92920', }, { type: messageTypes.EDIT_ENTRY, threadID: '84695', creatorID: '83928', time: 1682083374471, entryID: '92917', date: '2023-04-02', text: 'text', id: '92950', }, { type: messageTypes.DELETE_ENTRY, threadID: '86033', creatorID: '83928', time: 1682083220296, entryID: '92904', date: '2023-04-02', text: 'text', id: '92932', }, { type: messageTypes.RESTORE_ENTRY, id: '92962', threadID: '86033', time: 1682083414244, creatorID: '83928', entryID: '92904', date: '2023-04-02', text: 'text', }, { type: messageTypes.UNSUPPORTED, threadID: '87080', creatorID: '256', time: 1640733462322, robotext: 'unsupported message', unsupportedMessageInfo: { type: 105, threadID: '97489', creatorID: '256', time: 1640773011289, id: '97672', }, id: '97730', }, { type: messageTypes.IMAGES, threadID: '92796', creatorID: '83928', time: 1682083469079, media: [ { id: '92974', uri: 'http://0.0.0.0:3000/comm/upload/92974/ff3d02ded71e2762', type: 'photo', dimensions: { width: 220, height: 220, }, }, ], localID: 'local0', id: '92976', }, { type: messageTypes.MULTIMEDIA, threadID: '89644', creatorID: '83853', time: 1682076177257, media: [ { type: 'video', id: '92769', uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66', dimensions: { width: 480, height: 270, }, thumbnailID: '92770', thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db', }, ], id: '92771', }, { type: messageTypes.LEGACY_UPDATE_RELATIONSHIP, threadID: '92796', creatorID: '83928', targetID: '83853', time: 1682083716312, operation: 'request_sent', id: '93039', }, { type: messageTypes.SIDEBAR_SOURCE, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessage: { type: 0, id: '92816', threadID: '92796', time: 1682076737518, creatorID: '83928', text: 'text', }, id: '93049', }, { type: messageTypes.CREATE_SIDEBAR, threadID: '93044', creatorID: '83928', time: 1682083756831, sourceMessageAuthorID: '83928', initialThreadState: { name: 'text', parentThreadID: '92796', color: 'aa4b4b', memberIDs: ['83853', '83928'], }, id: '93050', }, { type: messageTypes.REACTION, threadID: '86033', localID: 'local8', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', reaction: '😂', action: 'add_reaction', id: '92943', }, { type: messageTypes.EDIT_MESSAGE, threadID: '86033', creatorID: '83928', time: 1682083295820, targetMessageID: '91607', text: 'text', id: '92943', }, { type: messageTypes.TOGGLE_PIN, threadID: '86033', targetMessageID: '91607', action: 'pin', pinnedContent: 'text', creatorID: '83928', time: 1682083295820, id: '92943', }, ]; describe('message validation', () => { for (const validatorMessageTypeName in messageTypes) { const validatorMessageType = messageTypes[validatorMessageTypeName]; const validator = messageSpecs[validatorMessageType].validator; for (const message of messages) { const messageTypeName = _findKey(e => e === message.type)(messageTypes); if (validatorMessageType === message.type) { it(`${validatorMessageTypeName} should validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(true); }); } else if ( !( (validatorMessageType === messageTypes.IMAGES && message.type === messageTypes.MULTIMEDIA) || (validatorMessageType === messageTypes.MULTIMEDIA && message.type === messageTypes.IMAGES) || (validatorMessageType === messageTypes.UPDATE_RELATIONSHIP && message.type === messageTypes.LEGACY_UPDATE_RELATIONSHIP) || (validatorMessageType === messageTypes.LEGACY_UPDATE_RELATIONSHIP && message.type === messageTypes.UPDATE_RELATIONSHIP) ) ) { it(`${validatorMessageTypeName} shouldn't validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(false); }); } } } }); const thread = { id: '85171', type: threadTypes.GENESIS_PERSONAL, name: '', description: '', color: '6d49ab', creationTime: 1675887298557, parentThreadID: '1', members: [ { id: '256', role: null, permissions: { know_of: { value: true, source: '1', }, visible: { value: true, source: '1', }, voiced: { value: true, source: '1', }, edit_entries: { value: true, source: '1', }, edit_thread: { value: true, source: '1', }, edit_thread_description: { value: true, source: '1', }, edit_thread_color: { value: true, source: '1', }, delete_thread: { value: true, source: '1', }, create_subthreads: { value: true, source: '1', }, create_sidebars: { value: true, source: '1', }, join_thread: { value: true, source: '1', }, edit_permissions: { value: true, source: '1', }, add_members: { value: true, source: '1', }, remove_members: { value: true, source: '1', }, change_role: { value: true, source: '1', }, leave_thread: { value: false, source: null, }, react_to_message: { value: false, source: null, }, edit_message: { value: false, source: null, }, manage_pins: { value: true, source: '1', }, }, isSender: false, }, { id: '83853', role: '85172', permissions: { know_of: { value: true, source: '85171', }, visible: { value: true, source: '85171', }, voiced: { value: true, source: '85171', }, edit_entries: { value: true, source: '85171', }, edit_thread: { value: true, source: '85171', }, edit_thread_description: { value: true, source: '85171', }, edit_thread_color: { value: true, source: '85171', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '85171', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: '85171', }, edit_message: { value: true, source: '85171', }, manage_pins: { value: false, source: null, }, }, isSender: true, }, ], roles: { '85172': { id: '85172', name: 'Members', permissions: { know_of: true, visible: true, voiced: true, react_to_message: true, edit_message: true, edit_entries: true, edit_thread: true, edit_thread_color: true, edit_thread_description: true, create_sidebars: true, descendant_open_know_of: true, descendant_open_visible: true, child_open_join_thread: true, }, isDefault: true, }, }, currentUser: { role: '85172', permissions: { know_of: { value: true, source: '85171', }, visible: { value: true, source: '85171', }, voiced: { value: true, source: '85171', }, edit_entries: { value: true, source: '85171', }, edit_thread: { value: true, source: '85171', }, edit_thread_description: { value: true, source: '85171', }, edit_thread_color: { value: true, source: '85171', }, delete_thread: { value: false, source: null, }, create_subthreads: { value: false, source: null, }, create_sidebars: { value: true, source: '85171', }, join_thread: { value: false, source: null, }, edit_permissions: { value: false, source: null, }, add_members: { value: false, source: null, }, remove_members: { value: false, source: null, }, change_role: { value: false, source: null, }, leave_thread: { value: false, source: null, }, react_to_message: { value: true, source: '85171', }, edit_message: { value: true, source: '85171', }, manage_pins: { value: false, source: null, }, }, subscription: { home: true, pushNotifs: true, }, unread: false, }, repliesCount: 0, containingThreadID: '1', community: '1', pinnedCount: 0, }; describe('thread validation', () => { it('should validate correct thread', () => { - expect(legacyRawThreadInfoValidator.is(thread)).toBe(true); + expect(legacyThinRawThreadInfoValidator.is(thread)).toBe(true); }); it('should not validate incorrect thread', () => { expect( - legacyRawThreadInfoValidator.is({ ...thread, creationTime: undefined }), + legacyThinRawThreadInfoValidator.is({ + ...thread, + creationTime: undefined, + }), ).toBe(false); }); }); const entry = { id: '92860', threadID: '85068', text: 'text', year: 2023, month: 4, day: 2, creationTime: 1682082939882, creatorID: '83853', deleted: false, }; describe('entry validation', () => { it('should validate correct entry', () => { expect(rawEntryInfoValidator.is(entry)).toBe(true); }); it('should not validate incorrect entry', () => { expect(rawEntryInfoValidator.is({ ...entry, threadID: 0 })).toBe(false); }); }); const updates = [ { type: updateTypes.DELETE_ACCOUNT, id: '98424', time: 1640870111106, deletedUserID: '98262', }, { type: updateTypes.UPDATE_THREAD, id: '97948', time: 1640868525494, threadInfo: thread, }, { type: updateTypes.UPDATE_THREAD_READ_STATUS, id: '98002', time: 1640869373326, threadID: '83794', unread: true, }, { type: updateTypes.DELETE_THREAD, id: '98208', time: 1640869773339, threadID: '97852', }, { type: updateTypes.JOIN_THREAD, id: '98126', time: 1640869494461, threadInfo: thread, rawMessageInfos: messages, truncationStatus: 'exhaustive', rawEntryInfos: [entry], }, { type: updateTypes.BAD_DEVICE_TOKEN, id: '98208', time: 1640869773495, deviceToken: 'some-device-token', }, { type: updateTypes.UPDATE_ENTRY, id: '98233', time: 1640869844908, entryInfo: entry, }, { type: updateTypes.UPDATE_CURRENT_USER, id: '98237', time: 1640869934058, currentUserInfo: { id: '256', username: 'ashoat', }, }, { type: updateTypes.UPDATE_USER, id: '97988', time: 1640869211822, updatedUserID: '86565', }, ]; describe('server update validation', () => { for (const validatorUpdateType of values(updateTypes)) { const validator = updateSpecs[validatorUpdateType].infoValidator; const validatorUpdateTypeName = _findKey( e => e === Number(validatorUpdateType), )(updateTypes); for (const update of updates) { const updateTypeName = _findKey(e => e === update.type)(updateTypes); if (Number(validatorUpdateType) === update.type) { it(`${validatorUpdateTypeName} should validate ${updateTypeName}`, () => { expect(validator.is(update)).toBe(true); }); } else { it(`${validatorUpdateTypeName} shouldn't validate ${updateTypeName}`, () => { expect(validator.is(update)).toBe(false); }); } } } }); describe('socket message validation', () => { const socketMessages = [ { type: serverSocketMessageTypes.STATE_SYNC, responseTo: 0, payload: { type: 1, messagesResult: { rawMessageInfos: messages, truncationStatuses: { '86033': 'unchanged' }, currentAsOf: 1683296863468, }, updatesResult: { newUpdates: updates, currentAsOf: 1683296863489, }, deltaEntryInfos: [], deletedEntryIDs: [], userInfos: [], }, }, { type: serverSocketMessageTypes.REQUESTS, payload: { serverRequests: [ { type: 6, hashesToCheck: { threadInfos: 3311950643, entryInfos: 3191324567, currentUserInfo: 820850779, userInfos: 707653884, }, }, ], }, }, { type: serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE, responseTo: 194, payload: { unfocusedToUnread: [] }, }, { type: serverSocketMessageTypes.PONG, responseTo: 190 }, { type: 6, payload: { updatesResult: { currentAsOf: 1683298141720, newUpdates: [ { type: 1, id: '94428', time: 1683298141720, threadInfo: thread, }, ], }, userInfos: [], }, }, { type: serverSocketMessageTypes.MESSAGES, payload: { messagesResult: { rawMessageInfos: messages, truncationStatuses: { '86033': 'unchanged' }, currentAsOf: 1683298141707, }, }, }, { type: serverSocketMessageTypes.API_RESPONSE, responseTo: 209, payload: { rawMessageInfos: messages, truncationStatuses: { '1': 'exhaustive' }, userInfos: {}, }, }, ]; const validatorByMessageType = { [serverSocketMessageTypes.STATE_SYNC]: serverStateSyncServerSocketMessageValidator, [serverSocketMessageTypes.REQUESTS]: serverRequestsServerSocketMessageValidator, [serverSocketMessageTypes.ERROR]: errorServerSocketMessageValidator, [serverSocketMessageTypes.AUTH_ERROR]: authErrorServerSocketMessageValidator, [serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE]: activityUpdateResponseServerSocketMessageValidator, [serverSocketMessageTypes.PONG]: pongServerSocketMessageValidator, [serverSocketMessageTypes.UPDATES]: serverUpdatesServerSocketMessageValidator, [serverSocketMessageTypes.MESSAGES]: messagesServerSocketMessageValidator, [serverSocketMessageTypes.API_RESPONSE]: apiResponseServerSocketMessageValidator, }; for (const validatorMessageType in validatorByMessageType) { const validator = validatorByMessageType[validatorMessageType]; const validatorMessageTypeName = _findKey( e => e === Number(validatorMessageType), )(serverSocketMessageTypes); for (const message of socketMessages) { const messageTypeName = _findKey(e => e === message.type)( serverSocketMessageTypes, ); if (Number(validatorMessageType) === message.type) { it(`${validatorMessageTypeName} should validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(true); }); } else { it(`${validatorMessageTypeName} shouldn't validate ${messageTypeName}`, () => { expect(validator.is(message)).toBe(false); }); } } } }); diff --git a/lib/types/validators/redux-state-validators.js b/lib/types/validators/redux-state-validators.js index 225af4f60..06417eedd 100644 --- a/lib/types/validators/redux-state-validators.js +++ b/lib/types/validators/redux-state-validators.js @@ -1,39 +1,39 @@ // @flow import t, { type TInterface } from 'tcomb'; -import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tShape, tID } from '../../utils/validation-utils.js'; import { entryStoreValidator } from '../entry-types.js'; import { inviteLinksStoreValidator } from '../link-types.js'; import { messageStoreValidator } from '../message-types.js'; import { webNavInfoValidator } from '../nav-types.js'; import type { WebInitialKeyserverInfo, ServerWebInitialReduxStateResponse, } from '../redux-types.js'; import type { ThreadStore } from '../thread-types'; import { currentUserInfoValidator, userInfosValidator } from '../user-types.js'; const initialKeyserverInfoValidator = tShape({ sessionID: t.maybe(t.String), updatesCurrentAsOf: t.Number, }); export const threadStoreValidator: TInterface = tShape({ - threadInfos: t.dict(tID, mixedRawThreadInfoValidator), + threadInfos: t.dict(tID, mixedThinRawThreadInfoValidator), }); export const initialReduxStateValidator: TInterface = tShape({ navInfo: webNavInfoValidator, currentUserInfo: currentUserInfoValidator, entryStore: entryStoreValidator, threadStore: threadStoreValidator, userInfos: userInfosValidator, messageStore: messageStoreValidator, pushApiPublicKey: t.maybe(t.String), inviteLinksStore: inviteLinksStoreValidator, keyserverInfo: initialKeyserverInfoValidator, }); diff --git a/lib/types/validators/thread-validators.js b/lib/types/validators/thread-validators.js index 15b31e349..3e6653de5 100644 --- a/lib/types/validators/thread-validators.js +++ b/lib/types/validators/thread-validators.js @@ -1,84 +1,84 @@ // @flow import t from 'tcomb'; import type { TInterface } from 'tcomb'; -import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tShape, tID } from '../../utils/validation-utils.js'; import { mediaValidator } from '../media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from '../message-types.js'; import { type ChangeThreadSettingsResult, type LeaveThreadResult, type NewThreadResponse, type ThreadJoinResult, type ThreadFetchMediaResult, type ToggleMessagePinResult, type RoleModificationResult, type RoleDeletionResult, } from '../thread-types.js'; import { serverUpdateInfoValidator } from '../update-types.js'; import { userInfosValidator } from '../user-types.js'; export const leaveThreadResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), userInfos: userInfosValidator, newThreadID: tID, }); export const threadJoinResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); export const roleModificationResultValidator: TInterface = tShape({ - threadInfo: t.maybe(mixedRawThreadInfoValidator), + threadInfo: t.maybe(mixedThinRawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); export const roleDeletionResultValidator: TInterface = tShape({ - threadInfo: t.maybe(mixedRawThreadInfoValidator), + threadInfo: t.maybe(mixedThinRawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); diff --git a/lib/types/validators/user-validators.js b/lib/types/validators/user-validators.js index 240d57788..c8b4dd905 100644 --- a/lib/types/validators/user-validators.js +++ b/lib/types/validators/user-validators.js @@ -1,87 +1,87 @@ // @flow import t, { type TInterface, type TUnion, type TEnums } from 'tcomb'; import { policyTypeValidator } from '../../facts/policies.js'; -import { mixedRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../../permissions/minimally-encoded-raw-thread-info-validators.js'; import { tShape, tID, tUserID } from '../../utils/validation-utils.js'; import type { LogOutResponse, RegisterResponse, ServerLogInResponse, ClaimUsernameResponse, } from '../account-types.js'; import { type ClientAvatar, clientAvatarValidator, type UpdateUserAvatarResponse, } from '../avatar-types.js'; import { rawEntryInfoValidator } from '../entry-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from '../message-types.js'; import { type SubscriptionUpdateResponse, threadSubscriptionValidator, } from '../subscription-types.js'; import { createUpdatesResultValidator } from '../update-types.js'; import { loggedOutUserInfoValidator, loggedInUserInfoValidator, userInfoValidator, } from '../user-types.js'; export const registerResponseValidator: TInterface = tShape({ id: tUserID, rawMessageInfos: t.list(rawMessageInfoValidator), currentUserInfo: loggedInUserInfoValidator, cookieChange: tShape({ - threadInfos: t.dict(tID, mixedRawThreadInfoValidator), + threadInfos: t.dict(tID, mixedThinRawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), }); export const logOutResponseValidator: TInterface = tShape({ currentUserInfo: loggedOutUserInfoValidator, }); export const logInResponseValidator: TInterface = tShape({ currentUserInfo: loggedInUserInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: t.list(userInfoValidator), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), serverTime: t.Number, cookieChange: tShape({ - threadInfos: t.dict(tID, mixedRawThreadInfoValidator), + threadInfos: t.dict(tID, mixedThinRawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), }); export const subscriptionUpdateResponseValidator: TInterface = tShape({ threadSubscription: threadSubscriptionValidator, }); export const updateUserAvatarResponseValidator: TInterface = tShape({ updates: createUpdatesResultValidator, }); export const updateUserAvatarResponderValidator: TUnion< ?ClientAvatar | UpdateUserAvatarResponse, > = t.union([ t.maybe(clientAvatarValidator), updateUserAvatarResponseValidator, ]); export const claimUsernameResponseValidator: TInterface = tShape({ message: t.String, signature: t.String, }); diff --git a/lib/utils/user-info-extraction-utils.js b/lib/utils/user-info-extraction-utils.js index 565b2a0ff..eaa94ddc2 100644 --- a/lib/utils/user-info-extraction-utils.js +++ b/lib/utils/user-info-extraction-utils.js @@ -1,92 +1,92 @@ // @flow import _memoize from 'lodash/memoize.js'; import t, { type TType, type TInterface } from 'tcomb'; import { processNewUserIDsActionType } from '../actions/user-actions.js'; import type { CallSingleKeyserverEndpointResponse } from '../keyserver-conn/call-single-keyserver-endpoint.js'; -import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { mixedThinRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import type { Endpoint } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { MixedRawThreadInfos } from '../types/thread-types.js'; import { userInfoValidator } from '../types/user-types.js'; import type { UserInfo } from '../types/user-types.js'; import { endpointValidators } from '../types/validators/endpoint-validators.js'; import { extractUserIDsFromPayload } from '../utils/conversion-utils.js'; import { tID, tShape } from '../utils/validation-utils.js'; type AdditionalCookieChange = { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, +cookieInvalidated?: boolean, sessionID?: string, cookie?: string, }; type AdditionalResponseFields = { +cookieChange: AdditionalCookieChange, +error?: string, +payload?: Object, +success: boolean, }; const additionalResponseFieldsValidator = tShape({ cookieChange: t.maybe( tShape({ - threadInfos: t.dict(tID, mixedRawThreadInfoValidator), + threadInfos: t.dict(tID, mixedThinRawThreadInfoValidator), userInfos: t.list(userInfoValidator), cookieInvalidated: t.maybe(t.Boolean), sessionID: t.maybe(t.String), cookie: t.maybe(t.String), }), ), error: t.maybe(t.String), payload: t.maybe(t.Object), success: t.maybe(t.Boolean), }); function extendResponderValidatorBase(inputValidator: TType): TType { if (inputValidator.meta.kind === 'union') { const newTypes = []; for (const innerValidator of inputValidator.meta.types) { const newInnerValidator = extendResponderValidatorBase(innerValidator); newTypes.push(newInnerValidator); } return t.union(newTypes); } else if (inputValidator.meta.kind === 'interface') { const recastValidator: TInterface = (inputValidator: any); return (tShape({ ...recastValidator.meta.props, ...additionalResponseFieldsValidator.meta.props, }): any); } else if (inputValidator.meta.kind === 'maybe') { const typeObj = extendResponderValidatorBase(inputValidator.meta.type); return (t.maybe(typeObj): any); } else if (inputValidator.meta.kind === 'subtype') { return extendResponderValidatorBase(inputValidator.meta.type); } return inputValidator; } const extendResponderValidator = _memoize(extendResponderValidatorBase); function extractAndPersistUserInfosFromEndpointResponse( message: CallSingleKeyserverEndpointResponse, endpoint: Endpoint, dispatch: Dispatch, ): void { const extendedValidator = extendResponderValidator( endpointValidators[endpoint].validator, ); const newUserIDs = extractUserIDsFromPayload(extendedValidator, message); if (newUserIDs.length > 0) { dispatch({ type: processNewUserIDsActionType, payload: { userIDs: newUserIDs }, }); } } export { extractAndPersistUserInfosFromEndpointResponse };