diff --git a/lib/types/avatar-types.js b/lib/types/avatar-types.js --- a/lib/types/avatar-types.js +++ b/lib/types/avatar-types.js @@ -1,10 +1,19 @@ // @flow +import t, { type TUnion } from 'tcomb'; + +import { tShape, tString } from '../utils/validation-utils.js'; + export type EmojiAvatarDBContent = { +type: 'emoji', +emoji: string, +color: string, // hex, without "#" or "0x" }; +const emojiAvatarDBContentValidator = tShape({ + type: tString('emoji'), + emoji: t.String, + color: t.String, +}); export type ImageAvatarDBContent = { +type: 'image', @@ -14,6 +23,7 @@ export type ENSAvatarDBContent = { +type: 'ens', }; +const ensAvatarDBContentValidator = tShape({ type: tString('ens') }); export type AvatarDBContent = | EmojiAvatarDBContent @@ -27,15 +37,28 @@ | UpdateUserAvatarRemoveRequest; export type ClientEmojiAvatar = EmojiAvatarDBContent; +const clientEmojiAvatarValidator = emojiAvatarDBContentValidator; + export type ClientImageAvatar = { +type: 'image', +uri: string, }; +const clientImageAvatarValidator = tShape({ + type: tString('image'), + uri: t.String, +}); + export type ClientENSAvatar = ENSAvatarDBContent; +const clientENSAvatarValidator = ensAvatarDBContentValidator; export type ClientAvatar = | ClientEmojiAvatar | ClientImageAvatar | ClientENSAvatar; +export const clientAvatarValidator: TUnion = t.union([ + clientEmojiAvatarValidator, + clientImageAvatarValidator, + clientENSAvatarValidator, +]); export type ResolvedClientAvatar = ClientEmojiAvatar | ClientImageAvatar; diff --git a/lib/types/subscription-types.js b/lib/types/subscription-types.js --- a/lib/types/subscription-types.js +++ b/lib/types/subscription-types.js @@ -1,6 +1,10 @@ // @flow +import _mapValues from 'lodash/fp/mapValues.js'; +import t, { type TInterface } from 'tcomb'; + import type { Shape } from './core.js'; +import { tShape } from '../utils/validation-utils.js'; export const threadSubscriptions = Object.freeze({ home: 'home', @@ -12,6 +16,9 @@ () => boolean, >; +export const threadSubscriptionValidator: TInterface = + tShape(_mapValues(() => t.Boolean)(threadSubscriptions)); + export type SubscriptionUpdateRequest = { threadID: string, updatedFields: Shape, diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,11 +1,13 @@ // @flow import invariant from 'invariant'; +import t, { type TInterface } from 'tcomb'; -import type { - AvatarDBContent, - ClientAvatar, - UpdateUserAvatarRequest, +import { + type AvatarDBContent, + type ClientAvatar, + type UpdateUserAvatarRequest, + clientAvatarValidator, } from './avatar-types.js'; import type { Shape } from './core.js'; import type { CalendarQuery, RawEntryInfo } from './entry-types.js'; @@ -14,10 +16,15 @@ RawMessageInfo, MessageTruncationStatuses, } from './message-types.js'; -import type { ThreadSubscription } from './subscription-types.js'; +import { + type ThreadSubscription, + threadSubscriptionValidator, +} from './subscription-types.js'; import type { ServerUpdateInfo, ClientUpdateInfo } from './update-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import type { ThreadEntity } from '../utils/entity-text.js'; +import { values } from '../utils/objects.js'; +import { tNumEnum, tBool, tID, tShape } from '../utils/validation-utils.js'; export const threadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) @@ -68,6 +75,8 @@ ); return threadType; } +const threadTypeValidator = tNumEnum(values(threadTypes)); + export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, @@ -138,6 +147,7 @@ ); return ourThreadPermissions; } +const threadPermissionValidator = t.enums.of(values(threadPermissions)); export const threadPermissionPropagationPrefixes = Object.freeze({ DESCENDANT: 'descendant_', @@ -165,14 +175,24 @@ | { +value: true, +source: string } | { +value: false, +source: null }; +const threadPermissionInfoValidator = t.union([ + tShape({ value: tBool(true), source: t.String }), + tShape({ value: tBool(false), source: t.Nil }), +]); + export type ThreadPermissionsBlob = { +[permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { +[permission: string]: boolean }; +const threadRolePermissionsBlobValidator = t.dict(t.String, t.Boolean); export type ThreadPermissionsInfo = { +[permission: ThreadPermission]: ThreadPermissionInfo, }; +const threadPermissionsInfoValidator = t.dict( + threadPermissionValidator, + threadPermissionInfoValidator, +); export type MemberInfo = { +id: string, @@ -180,6 +200,12 @@ +permissions: ThreadPermissionsInfo, +isSender: boolean, }; +const memberInfoValidator = tShape({ + id: t.String, + role: t.maybe(t.String), + permissions: threadPermissionsInfoValidator, + isSender: t.Boolean, +}); export type RelativeMemberInfo = { ...MemberInfo, @@ -193,6 +219,12 @@ +permissions: ThreadRolePermissionsBlob, +isDefault: boolean, }; +const roleInfoValidator = tShape({ + id: tID, + name: t.String, + permissions: threadRolePermissionsBlobValidator, + isDefault: t.Boolean, +}); export type ThreadCurrentUserInfo = { +role: ?string, @@ -200,6 +232,12 @@ +subscription: ThreadSubscription, +unread: ?boolean, }; +const threadCurrentUserInfoValidator = tShape({ + role: t.maybe(t.String), + permissions: threadPermissionsInfoValidator, + subscription: threadSubscriptionValidator, + unread: t.maybe(t.Boolean), +}); export type RawThreadInfo = { +id: string, @@ -219,6 +257,25 @@ +repliesCount: number, +pinnedCount?: number, }; +export const rawThreadInfoValidator: TInterface = + tShape({ + id: tID, + type: threadTypeValidator, + 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(memberInfoValidator), + roles: t.dict(t.String, roleInfoValidator), + currentUser: threadCurrentUserInfoValidator, + sourceMessageID: t.maybe(tID), + repliesCount: t.Number, + pinnedCount: t.maybe(t.Number), + }); export type ThreadInfo = { +id: string, diff --git a/lib/types/validation.test.js b/lib/types/validation.test.js --- a/lib/types/validation.test.js +++ b/lib/types/validation.test.js @@ -30,6 +30,7 @@ import { rawTogglePinMessageInfoValidator } from './messages/toggle-pin.js'; import { rawUnsupportedMessageInfoValidator } from './messages/unsupported.js'; import { rawUpdateRelationshipMessageInfoValidator } from './messages/update-relationship.js'; +import { threadTypes, rawThreadInfoValidator } from './thread-types.js'; describe('media validation', () => { const photo = { @@ -354,3 +355,316 @@ }); } }); + +describe('thread validation', () => { + const thread = { + id: '85171', + type: threadTypes.PERSONAL, + name: '', + description: '', + color: '6d49ab', + creationTime: 1675887298557, + parentThreadID: '1', + members: [ + { + id: '256', + role: null, + permissions: { + know_of: { + value: true, + source: '1', + }, + membership: { + value: false, + source: null, + }, + 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', + }, + membership: { + value: false, + source: null, + }, + 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', + }, + membership: { + value: false, + source: null, + }, + 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, + }; + + it('should validate correct thread', () => { + expect(rawThreadInfoValidator.is(thread)).toBe(true); + }); + it('should not validate incorrect thread', () => { + expect( + rawThreadInfoValidator.is({ ...thread, creationTime: undefined }), + ).toBe(false); + }); +});