diff --git a/lib/shared/farcaster/farcaster-conversation-types.js b/lib/shared/farcaster/farcaster-conversation-types.js new file mode 100644 --- /dev/null +++ b/lib/shared/farcaster/farcaster-conversation-types.js @@ -0,0 +1,186 @@ +// @flow + +import type { TInterface } from 'tcomb'; +import t from 'tcomb'; + +import type { FarcasterMessage } from './farcaster-messages-types.js'; +import { farcasterMessageValidator } from './farcaster-messages-types.js'; +import type { + FarcasterDCUser, + FarcasterDCUserMinimal, +} from './farcaster-user-types.js'; +import { + farcasterDCUserValidator, + farcasterDCUserMinimalValidator, +} from './farcaster-user-types.js'; +import { tShapeInexact } from '../../utils/validation-utils.js'; + +type FarcasterUnreadReactionMessage = { + +fid: number, + +displayName: string, + +username: string, + +reaction: string, + +timestamp: number, + +reactedMessageFid: number, + +reactedMessage: string, +}; +const farcasterUnreadReactionMessageValidator: TInterface = + tShapeInexact({ + fid: t.Number, + displayName: t.String, + username: t.String, + reaction: t.String, + timestamp: t.Number, + reactedMessageFid: t.Number, + reactedMessage: t.String, + }); + +type FarcasterInboxViewerContext = { + +category: 'default' | 'archived' | 'request', + +lastReadAt: number, + +muted: boolean, + +manuallyMarkedUnread: boolean, + +pinned: boolean, + +unreadCount: number, + +unreadMentionsCount: number, + +unreadReactionMessage?: FarcasterUnreadReactionMessage, + +counterParty?: FarcasterDCUser, + +tag?: 'automated' | 'new-user', +}; +const farcasterInboxViewerContextValidator: TInterface = + tShapeInexact({ + category: t.enums.of(['default', 'archived', 'request']), + lastReadAt: t.Number, + muted: t.Boolean, + manuallyMarkedUnread: t.Boolean, + pinned: t.Boolean, + unreadCount: t.Number, + unreadMentionsCount: t.Number, + unreadReactionMessage: t.maybe(farcasterUnreadReactionMessageValidator), + counterParty: t.maybe(farcasterDCUserValidator), + tag: t.maybe(t.enums.of(['automated', 'new-user'])), + }); + +export type FarcasterInboxConversation = { + +conversationId: string, + +name?: string, + +description?: string, + +photoUrl?: string, + +adminFids: $ReadOnlyArray, + +lastReadTime: number, + +lastMessage?: FarcasterMessage, + +isGroup: boolean, + +createdAt: number, + +viewerContext: FarcasterInboxViewerContext, +}; +const farcasterInboxConversationValidator: TInterface = + tShapeInexact({ + conversationId: t.String, + name: t.maybe(t.String), + description: t.maybe(t.String), + photoUrl: t.maybe(t.String), + adminFids: t.list(t.Number), + lastReadTime: t.Number, + lastMessage: t.maybe(farcasterMessageValidator), + isGroup: t.Boolean, + createdAt: t.Number, + viewerContext: farcasterInboxViewerContextValidator, + }); + +type FarcasterConversationViewerContext = { + +access: 'read' | 'read-write' | 'admin', + +category: 'default' | 'archived' | 'request', + +archived: boolean, + +lastReadAt: number, + +muted: boolean, + +manuallyMarkedUnread: boolean, + +pinned: boolean, + +unreadCount: number, + +unreadMentionsCount: number, + +unreadReactionMessage?: FarcasterUnreadReactionMessage, + +counterParty?: FarcasterDCUser, + +tag?: 'automated' | 'new-user', + +inviter?: FarcasterDCUserMinimal, +}; +const farcasterConversationViewerContextValidator: TInterface = + tShapeInexact({ + access: t.enums.of(['read', 'read-write', 'admin']), + category: t.enums.of(['default', 'archived', 'request']), + archived: t.Boolean, + lastReadAt: t.Number, + muted: t.Boolean, + manuallyMarkedUnread: t.Boolean, + pinned: t.Boolean, + unreadCount: t.Number, + unreadMentionsCount: t.Number, + unreadReactionMessage: t.maybe(farcasterUnreadReactionMessageValidator), + counterParty: t.maybe(farcasterDCUserValidator), + tag: t.maybe(t.enums.of(['automated', 'new-user'])), + inviter: t.maybe(farcasterDCUserMinimalValidator), + }); + +type FarcasterConversationGroupPreferences = { + +membersCanInvite: boolean, + +periodicallyValidateMemberships: boolean, + +onlyAdminsCanWrite: boolean, +}; +const farcasterConversationGroupPreferencesValidator: TInterface = + tShapeInexact({ + membersCanInvite: t.Boolean, + periodicallyValidateMemberships: t.Boolean, + onlyAdminsCanWrite: t.Boolean, + }); + +export type FarcasterConversation = { + +conversationId: string, + +name?: string, + +description?: string, + +photoUrl?: string, + +adminFids: $ReadOnlyArray, + +removedFids: $ReadOnlyArray, + +participants: $ReadOnlyArray, + +lastReadTime: number, + +selfLastReadTime: number, + +lastMessage?: FarcasterMessage, + +pinnedMessages: $ReadOnlyArray, + +hasPinnedMessages: boolean, + +isGroup: boolean, + +groupPreferences?: FarcasterConversationGroupPreferences, + +activeParticipantsCount: number, + +messageTTLDays: 1 | 7 | 30 | 365 | 'Infinity', + +createdAt: number, + +unreadCount: number, + +muted: boolean, + +hasMention: boolean, + +viewerContext: FarcasterConversationViewerContext, + ... +}; +const farcasterConversationValidator: TInterface = + tShapeInexact({ + conversationId: t.String, + name: t.maybe(t.String), + description: t.maybe(t.String), + photoUrl: t.maybe(t.String), + adminFids: t.list(t.Number), + removedFids: t.list(t.Number), + participants: t.list(farcasterDCUserValidator), + lastReadTime: t.Number, + selfLastReadTime: t.Number, + lastMessage: t.maybe(farcasterMessageValidator), + pinnedMessages: t.list(farcasterMessageValidator), + hasPinnedMessages: t.Boolean, + isGroup: t.Boolean, + groupPreferences: t.maybe(farcasterConversationGroupPreferencesValidator), + activeParticipantsCount: t.Number, + messageTTLDays: t.union([ + t.enums.of([1, 7, 30, 365]), + t.enums.of(['Infinity']), + ]), + createdAt: t.Number, + unreadCount: t.Number, + muted: t.Boolean, + hasMention: t.Boolean, + viewerContext: farcasterConversationViewerContextValidator, + }); + +export { farcasterInboxConversationValidator, farcasterConversationValidator }; diff --git a/lib/shared/farcaster/farcaster-messages-types.js b/lib/shared/farcaster/farcaster-messages-types.js new file mode 100644 --- /dev/null +++ b/lib/shared/farcaster/farcaster-messages-types.js @@ -0,0 +1,135 @@ +// @flow + +import type { TInterface } from 'tcomb'; +import t from 'tcomb'; + +import type { + FarcasterDCUserMinimal, + FarcasterMessageUserContext, +} from './farcaster-user-types.js'; +import { + farcasterDCUserMinimalValidator, + farcasterMessageUserContextValidator, +} from './farcaster-user-types.js'; +import { tShapeInexact } from '../../utils/validation-utils.js'; + +export const farcasterMessageTypes = Object.freeze({ + TEXT: 'text', + GROUP_NAME_CHANGE: 'group_name_change', + GROUP_MEMBERSHIP_ADDITION: 'group_membership_addition', + GROUP_MEMBERSHIP_REMOVAL: 'group_membership_removal', + PIN_MESSAGE: 'pin_message', + MESSAGE_TTL_CHANGE: 'message_ttl_change', + RICH_ANNOUNCEMENT: 'rich_announcement', +}); +export type FarcasterMessageType = $Values; + +type FarcasterReaction = { + +reaction: string, + +count: number, +}; +const farcasterReactionValidator: TInterface = tShapeInexact( + { + reaction: t.String, + count: t.Number, + }, +); + +type FarcasterMessageViewerContext = { + +isLastReadMessage: boolean, + +focused: boolean, + +reactions: $ReadOnlyArray, + +isOptimistic?: boolean, +}; +const farcasterMessageViewerContextValidator: TInterface = + tShapeInexact({ + isLastReadMessage: t.Boolean, + focused: t.Boolean, + reactions: t.list(t.String), + isOptimistic: t.maybe(t.Boolean), + }); + +type FarcasterMessageMedia = { + +version: '2', + +width: number, + +height: number, + +staticRaster: string, + +mimeType?: string, +}; +const farcasterMessageMediaValidator: TInterface = + tShapeInexact({ + version: t.enums.of(['2']), + width: t.Number, + height: t.Number, + staticRaster: t.String, + mimeType: t.maybe(t.String), + }); + +type FarcasterMessageMetadata = { + +medias?: $ReadOnlyArray, + ... +}; +const farcasterMessageMetadataValidator: TInterface = + tShapeInexact({ + medias: t.maybe(t.list(farcasterMessageMediaValidator)), + }); + +type FarcasterMentionMetadata = { + +user: FarcasterDCUserMinimal, + +textIndex: number, + +length: number, +}; +const farcasterMentionMetadataValidator: TInterface = + tShapeInexact({ + user: farcasterDCUserMinimalValidator, + textIndex: t.Number, + length: t.Number, + }); + +export type FarcasterMessage = { + +conversationId: string, + +senderFid: number, + +messageId: string, + +serverTimestamp: number, + +type: FarcasterMessageType, + +message: string, + +hasMention: boolean, + +reactions: $ReadOnlyArray, + +metadata?: FarcasterMessageMetadata, + +viewerContext?: FarcasterMessageViewerContext, + +isPinned: boolean, + +isDeleted: boolean, + +senderContext: FarcasterMessageUserContext, + +actionTargetUserContext?: FarcasterMessageUserContext, + +isProgrammatic?: boolean, + +mentions?: $ReadOnlyArray, + ... +}; +const farcasterMessageValidator: TInterface = tShapeInexact({ + conversationId: t.String, + senderFid: t.Number, + messageId: t.String, + serverTimestamp: t.Number, + type: t.enums.of([ + 'text', + 'group_name_change', + 'group_membership_addition', + 'group_membership_removal', + 'pin_message', + 'message_ttl_change', + 'rich_announcement', + ]), + message: t.String, + hasMention: t.Boolean, + reactions: t.list(farcasterReactionValidator), + metadata: t.maybe(farcasterMessageMetadataValidator), + viewerContext: t.maybe(farcasterMessageViewerContextValidator), + isPinned: t.Boolean, + isDeleted: t.Boolean, + senderContext: farcasterMessageUserContextValidator, + actionTargetUserContext: t.maybe(farcasterMessageUserContextValidator), + isProgrammatic: t.maybe(t.Boolean), + mentions: t.maybe(t.list(farcasterMentionMetadataValidator)), +}); + +export { farcasterMessageValidator }; diff --git a/lib/shared/farcaster/farcaster-user-types.js b/lib/shared/farcaster/farcaster-user-types.js new file mode 100644 --- /dev/null +++ b/lib/shared/farcaster/farcaster-user-types.js @@ -0,0 +1,92 @@ +// @flow + +import type { TInterface } from 'tcomb'; +import t from 'tcomb'; + +import { tShapeInexact } from '../../utils/validation-utils.js'; + +type FarcasterProfilePicture = { + +url: string, + +verified: boolean, +}; +const farcasterProfilePictureValidator: TInterface = + tShapeInexact({ + url: t.String, + verified: t.Boolean, + }); + +export type FarcasterMessageUserContext = { + +fid: number, + +username?: string, + +displayName: string, + +pfp?: FarcasterProfilePicture, + ... +}; +const farcasterMessageUserContextValidator: TInterface = + tShapeInexact({ + fid: t.Number, + username: t.maybe(t.String), + displayName: t.String, + pfp: t.maybe(farcasterProfilePictureValidator), + }); + +export type FarcasterDCUserMinimal = { + +fid: number, + +username?: string, + +displayName: string, + +pfp?: FarcasterProfilePicture, + ... +}; +const farcasterDCUserMinimalValidator: TInterface = + tShapeInexact({ + fid: t.Number, + username: t.maybe(t.String), + displayName: t.String, + pfp: t.maybe(farcasterProfilePictureValidator), + }); + +type FarcasterDCUserViewerContext = { + +canSendDirectCasts?: boolean, + +canAddToGroupDirectly?: boolean, + +nerfed?: boolean, + +invisible?: boolean, + +blocking?: boolean, + +blockedBy?: boolean, + +enableNotifications?: boolean, + ... +}; +const farcasterDCUserViewerContextValidator: TInterface = + tShapeInexact({ + canSendDirectCasts: t.maybe(t.Boolean), + canAddToGroupDirectly: t.maybe(t.Boolean), + nerfed: t.maybe(t.Boolean), + invisible: t.maybe(t.Boolean), + blocking: t.maybe(t.Boolean), + blockedBy: t.maybe(t.Boolean), + enableNotifications: t.maybe(t.Boolean), + }); + +export type FarcasterDCUser = { + +fid: number, + +username?: string, + +displayName: string, + +pfp?: FarcasterProfilePicture, + +referrerUsername?: string, + +viewerContext?: FarcasterDCUserViewerContext, + ... +}; +const farcasterDCUserValidator: TInterface = tShapeInexact({ + fid: t.Number, + username: t.maybe(t.String), + displayName: t.String, + pfp: t.maybe(farcasterProfilePictureValidator), + referrerUsername: t.maybe(t.String), + viewerContext: t.maybe(farcasterDCUserViewerContextValidator), +}); + +export { + farcasterProfilePictureValidator, + farcasterMessageUserContextValidator, + farcasterDCUserMinimalValidator, + farcasterDCUserValidator, +}; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -40,6 +40,10 @@ return t.interface(spec, { strict: true }); } +function tShapeInexact(spec: TStructProps): TInterface { + return t.interface(spec, { strict: false }); +} + export type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); @@ -131,6 +135,7 @@ tString, tNumber, tShape, + tShapeInexact, tRegex, tNumEnum, tNull,