diff --git a/lib/types/identity-search/auth-message-types.js b/lib/types/identity-search/auth-message-types.js index 79e69906b..24e4a6047 100644 --- a/lib/types/identity-search/auth-message-types.js +++ b/lib/types/identity-search/auth-message-types.js @@ -1,33 +1,33 @@ // @flow /* * This file defines types and validation for the auth message sent * from the client to the Identity Search WebSocket server. * The definitions in this file should remain in sync * with the structures defined in the corresponding * Rust file at `shared/identity_search_messages/src/messages/auth_messages.rs`. * * If you edit the definitions in one file, * please make sure to update the corresponding definitions in the other. * */ import type { TInterface } from 'tcomb'; import t from 'tcomb'; -import { tShape, tString } from '../../utils/validation-utils.js'; +import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; export type IdentitySearchAuthMessage = { +type: 'IdentitySearchAuthMessage', +userID: string, +deviceID: string, +accessToken: string, }; export const identityAuthMessageValidator: TInterface = tShape({ type: tString('IdentitySearchAuthMessage'), - userID: t.String, + userID: tUserID, deviceID: t.String, accessToken: t.String, }); diff --git a/lib/types/identity-search/search-response-types.js b/lib/types/identity-search/search-response-types.js index bb3b68d12..caca005ca 100644 --- a/lib/types/identity-search/search-response-types.js +++ b/lib/types/identity-search/search-response-types.js @@ -1,76 +1,76 @@ // @flow /* * This file defines types and validation for the search response message * sent from the Identity Search WebSocket server to client. * The definitions in this file should remain in sync * with the structures defined in the corresponding Rust file at * `shared/identity_search_messages/src/messages/search_response.rs`. * * If you edit the definitions in one file, * please make sure to update the corresponding definitions in the other. * */ import type { TInterface, TUnion } from 'tcomb'; import t from 'tcomb'; -import { tShape, tString } from '../../utils/validation-utils.js'; +import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; export type IdentitySearchFailure = { +id: string, +error: string, }; export const identityFailureValidator: TInterface = tShape({ id: t.String, error: t.String, }); export type IdentitySearchUser = { +userID: string, +username: string, }; export const identitySearchUserValidator: TInterface = tShape({ - userID: t.String, + userID: tUserID, username: t.String, }); export type IdentitySearchResult = { +id: string, +hits: $ReadOnlyArray, }; export const identitySearchResultValidator: TInterface = tShape({ id: t.String, hits: t.list(identitySearchUserValidator), }); type IdentitySearchResponseSuccess = { +type: 'Success', +data: IdentitySearchResult, }; type IdentitySearchResponseError = { +type: 'Error', +data: IdentitySearchFailure, }; export type IdentitySearchResponse = | IdentitySearchResponseSuccess | IdentitySearchResponseError; export const identitySearchResponseValidator: TUnion = t.union([ tShape({ type: tString('Success'), data: identitySearchResultValidator, }), tShape({ type: tString('Error'), data: identityFailureValidator, }), ]); diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 983a99267..35df2e65d 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,272 +1,272 @@ // @flow import t, { type TInterface, type TList, type TDict } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { currentUserInfoValidator, type CurrentUserInfo, } from './user-types.js'; -import { tShape } from '../utils/validation-utils.js'; +import { tUserID, tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating OutboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; // This type should not be altered without also updating InboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, }); export type UserDevicesOlmOutboundKeys = { +deviceID: string, +keys: ?DeviceOlmOutboundKeys, }; export type DeviceOlmInboundKeys = { +identityKeysBlob: IdentityKeysBlob, +signedPrekeys: SignedPrekeys, +payloadSignature: string, }; export const deviceOlmInboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, signedPrekeys: signedPrekeysValidator, payloadSignature: t.String, }); export type UserDevicesOlmInboundKeys = { +keys: { +[deviceID: string]: ?DeviceOlmInboundKeys, }, +username?: ?string, +walletAddress?: ?string, }; // This type should not be altered without also updating FarcasterUser in // keyserver/addons/rust-node-addon/src/identity_client/get_farcaster_users.rs export type FarcasterUser = { +userID: string, +username: string, +farcasterID: string, }; export const farcasterUserValidator: TInterface = tShape({ - userID: t.String, + userID: tUserID, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface = tShape({ keys: t.dict(t.String, t.maybe(deviceOlmInboundKeysValidator)), username: t.maybe(t.String), walletAddress: t.maybe(t.String), }); export interface IdentityServiceClient { // Only a primary device can initiate account deletion, and web cannot be a // primary device +deleteWalletUser?: () => Promise; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise; +logOut: () => Promise; +getKeyserverKeys: string => Promise; +registerPasswordUser?: ( username: string, password: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray>; +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise; // updating device list is possible only on Native // web cannot be a primary device, so there's no need to expose it to JS +updateDeviceList?: (newDeviceList: SignedDeviceList) => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface = tShape({ - userID: t.String, + userID: tUserID, accessToken: t.String, username: t.String, preRequestUserState: t.maybe(currentUserInfoValidator), }); export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export const rawDeviceListValidator: TInterface = tShape({ devices: t.list(t.String), timestamp: t.Number, }); export type UsersRawDeviceLists = { +[userID: string]: RawDeviceList, }; export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, // Current primary device signature. Absent for Identity Service generated // device lists. +curPrimarySignature?: string, // Previous primary device signature. Present only if primary device // has changed since last update. +lastPrimarySignature?: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(t.String), }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict = t.dict(t.String, signedDeviceListValidator); export type SignedNonce = { +nonce: string, +nonceSignature: string, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); diff --git a/lib/types/relationship-types.js b/lib/types/relationship-types.js index 5ea8ad92c..752e032e4 100644 --- a/lib/types/relationship-types.js +++ b/lib/types/relationship-types.js @@ -1,114 +1,119 @@ // @flow import type { TInterface, TRefinement } from 'tcomb'; import t from 'tcomb'; import type { AccountUserInfo } from './user-types.js'; import { values } from '../utils/objects.js'; -import { tNumEnum, tShape, tString } from '../utils/validation-utils.js'; +import { + tUserID, + tNumEnum, + tShape, + tString, +} from '../utils/validation-utils.js'; export const undirectedStatus = Object.freeze({ KNOW_OF: 0, FRIEND: 2, }); export type UndirectedStatus = $Values; export const directedStatus = Object.freeze({ PENDING_FRIEND: 1, BLOCKED: 3, }); export type DirectedStatus = $Values; export const userRelationshipStatus = Object.freeze({ REQUEST_SENT: 1, REQUEST_RECEIVED: 2, FRIEND: 3, BLOCKED_BY_VIEWER: 4, BLOCKED_VIEWER: 5, BOTH_BLOCKED: 6, }); export type UserRelationshipStatus = $Values; export const userRelationshipStatusValidator: TRefinement = tNumEnum( values(userRelationshipStatus), ); const traditionalRelationshipActions = Object.freeze({ FRIEND: 'friend', UNFRIEND: 'unfriend', BLOCK: 'block', UNBLOCK: 'unblock', }); const farcasterRelationshipActions = Object.freeze({ FARCASTER_MUTUAL: 'farcaster', }); export const relationshipActions = Object.freeze({ ...traditionalRelationshipActions, ...farcasterRelationshipActions, }); export type RelationshipAction = $Values; export const relationshipActionsList: $ReadOnlyArray = values(relationshipActions); export type TraditionalRelationshipAction = $Values< typeof traditionalRelationshipActions, >; export const traditionalRelationshipActionsList: $ReadOnlyArray = values(traditionalRelationshipActions); export const relationshipButtons = Object.freeze({ FRIEND: 'friend', UNFRIEND: 'unfriend', BLOCK: 'block', UNBLOCK: 'unblock', ACCEPT: 'accept', WITHDRAW: 'withdraw', REJECT: 'reject', }); export type RelationshipButton = $Values; export type TraditionalRelationshipRequest = { +action: TraditionalRelationshipAction, +userIDs: $ReadOnlyArray, }; export type FarcasterRelationshipRequest = { +action: 'farcaster', +userIDsToFID: { +[userID: string]: string }, }; export type RelationshipRequest = | TraditionalRelationshipRequest | FarcasterRelationshipRequest; export const farcasterRelationshipRequestValidator: TInterface = tShape({ action: tString('farcaster'), - userIDsToFID: t.dict(t.String, t.String), + userIDsToFID: t.dict(tUserID, t.String), }); type SharedRelationshipRow = { user1: string, user2: string, }; export type DirectedRelationshipRow = { ...SharedRelationshipRow, status: DirectedStatus, }; export type UndirectedRelationshipRow = { ...SharedRelationshipRow, status: UndirectedStatus, }; export type RelationshipErrors = Partial<{ invalid_user: string[], already_friends: string[], user_blocked: string[], }>; export type UserRelationships = { +friends: $ReadOnlyArray, +blocked: $ReadOnlyArray, }; diff --git a/lib/types/report-types.js b/lib/types/report-types.js index 9b6ac2244..a0173fef2 100644 --- a/lib/types/report-types.js +++ b/lib/types/report-types.js @@ -1,238 +1,242 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { type PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery } from './entry-types.js'; import { type MediaMission } from './media-types.js'; import type { AppState, BaseAction } from './redux-types.js'; import { type MixedRawThreadInfos } from './thread-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; -import { tPlatformDetails, tShape } from '../utils/validation-utils.js'; +import { + tPlatformDetails, + tShape, + tUserID, +} from '../utils/validation-utils.js'; export type EnabledReports = { +crashReports: boolean, +inconsistencyReports: boolean, +mediaReports: boolean, }; export type SupportedReports = $Keys; export const defaultEnabledReports: EnabledReports = { crashReports: false, inconsistencyReports: false, mediaReports: false, }; export const defaultDevEnabledReports: EnabledReports = { crashReports: true, inconsistencyReports: true, mediaReports: true, }; export type ReportStore = { +enabledReports: EnabledReports, +queuedReports: $ReadOnlyArray, }; export const reportTypes = Object.freeze({ ERROR: 0, THREAD_INCONSISTENCY: 1, ENTRY_INCONSISTENCY: 2, MEDIA_MISSION: 3, USER_INCONSISTENCY: 4, }); type ReportType = $Values; export function assertReportType(reportType: number): ReportType { invariant( reportType === 0 || reportType === 1 || reportType === 2 || reportType === 3 || reportType === 4, 'number is not ReportType enum', ); return reportType; } export type ErrorInfo = { componentStack: string, ... }; export type ErrorData = { error: Error, info?: ErrorInfo }; export type FlatErrorData = { errorMessage: string, stack?: string, componentStack?: ?string, }; export type ActionSummary = { +type: $PropertyType, +time: number, +summary: string, }; export type ThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: MixedRawThreadInfos, +action: BaseAction, +pollResult?: ?MixedRawThreadInfos, +pushResult: MixedRawThreadInfos, +lastActionTypes?: ?$ReadOnlyArray<$PropertyType>, +lastActions?: ?$ReadOnlyArray, +time?: ?number, }; export type EntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, +pollResult?: ?{ +[id: string]: RawEntryInfo }, +pushResult: { +[id: string]: RawEntryInfo }, +lastActionTypes?: ?$ReadOnlyArray<$PropertyType>, +lastActions?: ?$ReadOnlyArray, +time: number, }; export type UserInconsistencyReportShape = { +platformDetails: PlatformDetails, +action: BaseAction, +beforeStateCheck: UserInfos, +afterStateCheck: UserInfos, +lastActions: $ReadOnlyArray, +time: number, }; export type ErrorReportCreationRequest = { +type: 0, +platformDetails: PlatformDetails, +errors: $ReadOnlyArray, +preloadedState: AppState, +currentState: AppState, +actions: $ReadOnlyArray, }; export type ThreadInconsistencyReportCreationRequest = { ...ThreadInconsistencyReportShape, +type: 1, }; export type EntryInconsistencyReportCreationRequest = { ...EntryInconsistencyReportShape, +type: 2, }; export type MediaMissionReportCreationRequest = { +type: 3, +platformDetails: PlatformDetails, +time: number, // ms +mediaMission: MediaMission, +uploadServerID?: ?string, +uploadLocalID?: ?string, +mediaLocalID?: ?string, // deprecated +messageServerID?: ?string, +messageLocalID?: ?string, }; export type UserInconsistencyReportCreationRequest = { ...UserInconsistencyReportShape, +type: 4, }; export type ReportCreationRequest = | ErrorReportCreationRequest | ThreadInconsistencyReportCreationRequest | EntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type ClientThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: MixedRawThreadInfos, +action: BaseAction, +pushResult: MixedRawThreadInfos, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientEntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, +pushResult: { +[id: string]: RawEntryInfo }, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientErrorReportCreationRequest = { ...ErrorReportCreationRequest, +id: string, }; export type ClientThreadInconsistencyReportCreationRequest = { ...ClientThreadInconsistencyReportShape, +type: 1, +id: string, }; export type ClientEntryInconsistencyReportCreationRequest = { ...ClientEntryInconsistencyReportShape, +type: 2, +id: string, }; export type ClientMediaMissionReportCreationRequest = { ...MediaMissionReportCreationRequest, +id: string, }; export type ClientUserInconsistencyReportCreationRequest = { ...UserInconsistencyReportCreationRequest, +id: string, }; export type ClientReportCreationRequest = | ClientErrorReportCreationRequest | ClientThreadInconsistencyReportCreationRequest | ClientEntryInconsistencyReportCreationRequest | ClientMediaMissionReportCreationRequest | ClientUserInconsistencyReportCreationRequest; export type QueueReportsPayload = { +reports: $ReadOnlyArray, }; export type ClearDeliveredReportsPayload = { +reports: $ReadOnlyArray, }; export type ReportCreationResponse = { +id: string, }; // Reports Service specific types export type ReportsServiceSendReportsRequest = | ClientReportCreationRequest | $ReadOnlyArray; export type ReportsServiceSendReportsResponse = { +reportIDs: $ReadOnlyArray, }; export type ReportsServiceSendReportsAction = ( request: ReportsServiceSendReportsRequest, ) => Promise; // Keyserver specific types type ReportInfo = { +id: string, +viewerID: string, +platformDetails: PlatformDetails, +creationTime: number, }; export const reportInfoValidator: TInterface = tShape({ id: t.String, - viewerID: t.String, + viewerID: tUserID, platformDetails: tPlatformDetails, creationTime: t.Number, }); export type FetchErrorReportInfosRequest = { +cursor: ?string, }; export type FetchErrorReportInfosResponse = { +reports: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export type ReduxToolsImport = { +preloadedState: AppState, +payload: $ReadOnlyArray, }; diff --git a/lib/types/request-types.js b/lib/types/request-types.js index 9049e2d86..e4429d1a5 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,298 +1,298 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate } from './activity-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { signedIdentityKeysBlobValidator } from './crypto-types.js'; import type { MessageSourceMetadata } from './db-ops-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 { ThreadInconsistencyReportShape, EntryInconsistencyReportShape, ClientThreadInconsistencyReportShape, ClientEntryInconsistencyReportShape, } 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 { tNumber, tShape, tID } from '../utils/validation-utils.js'; +import { tNumber, tShape, tID, tUserID } 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, }; export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; 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)), 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(t.String)), + deleteUserInfoIDs: t.maybe(t.list(tUserID)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; 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, }; type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: 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 = { +messageSourceMetadata?: MessageSourceMetadata, +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, }; export type OlmSessionInitializationInfo = { +prekey: string, +prekeySignature: string, +oneTimeKey: string, }; export const olmSessionInitializationInfoValidator: TInterface = tShape({ prekey: t.String, prekeySignature: t.String, oneTimeKey: t.String, }); export type GetOlmSessionInitializationDataResponse = { +signedIdentityKeysBlob: SignedIdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, }; export const getOlmSessionInitializationDataResponseValidator: TInterface = tShape({ signedIdentityKeysBlob: signedIdentityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, }); diff --git a/lib/types/tunnelbroker/peer-to-peer-message-types.js b/lib/types/tunnelbroker/peer-to-peer-message-types.js index 9833f2d96..f834ac6a1 100644 --- a/lib/types/tunnelbroker/peer-to-peer-message-types.js +++ b/lib/types/tunnelbroker/peer-to-peer-message-types.js @@ -1,116 +1,116 @@ // @flow import type { TInterface, TUnion } from 'tcomb'; import t from 'tcomb'; -import { tShape, tString } from '../../utils/validation-utils.js'; +import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; import { type EncryptedData, encryptedDataValidator } from '../crypto-types.js'; import { signedDeviceListValidator, type SignedDeviceList, } from '../identity-service-types.js'; export type SenderInfo = { +userID: string, +deviceID: string, }; const senderInfoValidator: TInterface = tShape({ - userID: t.String, + userID: tUserID, deviceID: t.String, }); export const peerToPeerMessageTypes = Object.freeze({ OUTBOUND_SESSION_CREATION: 'OutboundSessionCreation', ENCRYPTED_MESSAGE: 'EncryptedMessage', REFRESH_KEY_REQUEST: 'RefreshKeyRequest', QR_CODE_AUTH_MESSAGE: 'QRCodeAuthMessage', DEVICE_LIST_UPDATED: 'DeviceListUpdated', MESSAGE_PROCESSED: 'MessageProcessed', }); export type OutboundSessionCreation = { +type: 'OutboundSessionCreation', +senderInfo: SenderInfo, +encryptedData: EncryptedData, +sessionVersion: number, }; export const outboundSessionCreationValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION), senderInfo: senderInfoValidator, encryptedData: encryptedDataValidator, sessionVersion: t.Number, }); export type EncryptedMessage = { +type: 'EncryptedMessage', +senderInfo: SenderInfo, +encryptedData: EncryptedData, }; export const encryptedMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.ENCRYPTED_MESSAGE), senderInfo: senderInfoValidator, encryptedData: encryptedDataValidator, }); export type RefreshKeyRequest = { +type: 'RefreshKeyRequest', +deviceID: string, +numberOfKeys: number, }; export const refreshKeysRequestValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.REFRESH_KEY_REQUEST), deviceID: t.String, numberOfKeys: t.Number, }); export type QRCodeAuthMessage = { +type: 'QRCodeAuthMessage', +encryptedContent: string, }; export const qrCodeAuthMessageValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.QR_CODE_AUTH_MESSAGE), encryptedContent: t.String, }); export type DeviceListUpdated = { +type: 'DeviceListUpdated', +userID: string, +signedDeviceList: SignedDeviceList, }; export const deviceListUpdatedValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.DEVICE_LIST_UPDATED), - userID: t.String, + userID: tUserID, signedDeviceList: signedDeviceListValidator, }); export type MessageProcessed = { +type: 'MessageProcessed', +messageID: string, }; export const messageProcessedValidator: TInterface = tShape({ type: tString(peerToPeerMessageTypes.MESSAGE_PROCESSED), messageID: t.String, }); export type PeerToPeerMessage = | OutboundSessionCreation | EncryptedMessage | RefreshKeyRequest | QRCodeAuthMessage | DeviceListUpdated | MessageProcessed; export const peerToPeerMessageValidator: TUnion = t.union([ outboundSessionCreationValidator, encryptedMessageValidator, refreshKeysRequestValidator, qrCodeAuthMessageValidator, deviceListUpdatedValidator, messageProcessedValidator, ]); diff --git a/lib/types/tunnelbroker/qr-code-auth-message-types.js b/lib/types/tunnelbroker/qr-code-auth-message-types.js index 35089ed3a..d141d1d60 100644 --- a/lib/types/tunnelbroker/qr-code-auth-message-types.js +++ b/lib/types/tunnelbroker/qr-code-auth-message-types.js @@ -1,58 +1,58 @@ // @flow import type { TInterface, TUnion } from 'tcomb'; import t from 'tcomb'; -import { tShape, tString } from '../../utils/validation-utils.js'; +import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; export const qrCodeAuthMessageTypes = Object.freeze({ DEVICE_LIST_UPDATE_SUCCESS: 'DeviceListUpdateSuccess', SECONDARY_DEVICE_REGISTRATION_SUCCESS: 'SecondaryDeviceRegistrationSuccess', BACKUP_DATA_KEY_MESSAGE: 'BackupDataKeyMessage', }); export type DeviceListUpdateSuccess = { +type: 'DeviceListUpdateSuccess', +userID: string, +primaryDeviceID: string, }; export const deviceListUpdateSuccessValidator: TInterface = tShape({ type: tString(qrCodeAuthMessageTypes.DEVICE_LIST_UPDATE_SUCCESS), - userID: t.String, + userID: tUserID, primaryDeviceID: t.String, }); export type SecondaryDeviceRegistrationSuccess = { +type: 'SecondaryDeviceRegistrationSuccess', }; export const secondaryDeviceRegistrationSuccessValidator: TInterface = tShape({ type: tString(qrCodeAuthMessageTypes.SECONDARY_DEVICE_REGISTRATION_SUCCESS), }); export type BackupDataKeyMessage = { +type: 'BackupDataKeyMessage', +backupID: string, +backupDataKey: string, +backupLogDataKey: string, }; export const backupDataKeyMessageValidator: TInterface = tShape({ type: tString(qrCodeAuthMessageTypes.BACKUP_DATA_KEY_MESSAGE), backupID: t.String, backupDataKey: t.String, backupLogDataKey: t.String, }); export type QRCodeAuthMessagePayload = | DeviceListUpdateSuccess | SecondaryDeviceRegistrationSuccess | BackupDataKeyMessage; export const qrCodeAuthMessagePayloadValidator: TUnion = t.union([ deviceListUpdateSuccessValidator, secondaryDeviceRegistrationSuccessValidator, backupDataKeyMessageValidator, ]); diff --git a/lib/types/tunnelbroker/session-types.js b/lib/types/tunnelbroker/session-types.js index 19c3d59e4..434441e0e 100644 --- a/lib/types/tunnelbroker/session-types.js +++ b/lib/types/tunnelbroker/session-types.js @@ -1,52 +1,52 @@ // @flow import type { TInterface } from 'tcomb'; import t from 'tcomb'; -import { tShape, tString } from '../../utils/validation-utils.js'; +import { tShape, tString, tUserID } from '../../utils/validation-utils.js'; export type TunnelbrokerDeviceTypes = 'mobile' | 'web' | 'keyserver'; export type ConnectionInitializationMessage = { +type: 'ConnectionInitializationMessage', +deviceID: string, +accessToken: string, +userID: string, +notifyToken?: ?string, +deviceType: TunnelbrokerDeviceTypes, +deviceAppVersion?: ?string, +deviceOS?: ?string, }; export type AnonymousInitializationMessage = { +type: 'AnonymousInitializationMessage', +deviceID: string, +deviceType: TunnelbrokerDeviceTypes, +deviceAppVersion?: ?string, +deviceOS?: ?string, }; export type TunnelbrokerInitializationMessage = | ConnectionInitializationMessage | AnonymousInitializationMessage; export const connectionInitializationMessageValidator: TInterface = tShape({ type: tString('ConnectionInitializationMessage'), deviceID: t.String, accessToken: t.String, - userID: t.String, + userID: tUserID, notifyToken: t.maybe(t.String), deviceType: t.enums.of(['mobile', 'web', 'keyserver']), deviceAppVersion: t.maybe(t.String), deviceOS: t.maybe(t.String), }); export const anonymousInitializationMessageValidator: TInterface = tShape({ type: tString('AnonymousInitializationMessage'), deviceID: t.String, deviceType: t.enums.of(['mobile', 'web', 'keyserver']), deviceAppVersion: t.maybe(t.String), deviceOS: t.maybe(t.String), }); diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js index a3115b9ed..ad22bb879 100644 --- a/lib/utils/entity-text.js +++ b/lib/utils/entity-text.js @@ -1,684 +1,684 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import t, { type TInterface, type TUnion } from 'tcomb'; import type { GetENSNames } from './ens-helpers.js'; import type { GetFCNames } from './farcaster-helpers.js'; -import { tID, tShape, tString } from './validation-utils.js'; +import { tID, tShape, tString, tUserID } from './validation-utils.js'; import { useENSNames } from '../hooks/ens-cache.js'; import { useFCNames } from '../hooks/fc-cache.js'; import { threadNoun } from '../shared/thread-utils.js'; import { stringForUser } from '../shared/user-utils.js'; import type { ThreadInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { type ThreadType, threadTypes, threadTypeValidator, } from '../types/thread-types-enum.js'; import { basePluralize } from '../utils/text-utils.js'; export type UserEntity = { +type: 'user', +id: string, +username?: ?string, +isViewer?: ?boolean, +possessive?: ?boolean, // eg. `user's` instead of `user` }; export const userEntityValidator: TInterface = tShape({ type: tString('user'), - id: t.String, + id: tUserID, username: t.maybe(t.String), isViewer: t.maybe(t.Boolean), possessive: t.maybe(t.Boolean), }); // Comments explain how thread name will appear from user4's perspective export type ThreadEntity = | { +type: 'thread', +id: string, +name?: ?string, // displays threadInfo.name if set, or 'user1, user2, and user3' +display: 'uiName', // If uiName is EntityText, then at render time ThreadEntity will be // replaced with a pluralized list of uiName's UserEntities +uiName: $ReadOnlyArray | string, // If name isn't set and uiName is an array with only the viewer, then // just_you_string displays "just you" but viewer_username displays the // viewer's ENS-resolved username. Defaults to just_you_string +ifJustViewer?: 'just_you_string' | 'viewer_username', } | { +type: 'thread', +id: string, +name?: ?string, // displays threadInfo.name if set, or eg. 'this thread' or 'this chat' +display: 'shortName', +threadType?: ?ThreadType, +parentThreadID?: ?string, +alwaysDisplayShortName?: ?boolean, // don't default to name +subchannel?: ?boolean, // short name should be "subchannel" +possessive?: ?boolean, // eg. `this thread's` instead of `this thread` }; export const threadEntityValidator: TUnion = t.union([ tShape({ type: tString('thread'), id: tID, name: t.maybe(t.String), display: tString('uiName'), uiName: t.union([t.list(userEntityValidator), t.String]), ifJustViewer: t.maybe(t.enums.of(['just_you_string', 'viewer_username'])), }), tShape({ type: tString('thread'), id: tID, name: t.maybe(t.String), display: tString('shortName'), threadType: t.maybe(threadTypeValidator), parentThreadID: t.maybe(tID), alwaysDisplayShortName: t.maybe(t.Boolean), subchannel: t.maybe(t.Boolean), possessive: t.maybe(t.Boolean), }), ]); type ColorEntity = { +type: 'color', +hex: string, }; type FarcasterUserEntity = { +type: 'farcaster_user', +fid: string, +farcasterUsername?: ?string, }; type EntityTextComponent = | UserEntity | ThreadEntity | ColorEntity | FarcasterUserEntity | string; export type EntityText = $ReadOnlyArray; const entityTextFunction = ( strings: $ReadOnlyArray, ...entities: $ReadOnlyArray ) => { const result: EntityTextComponent[] = []; for (let i = 0; i < strings.length; i++) { const str = strings[i]; if (str) { result.push(str); } const entity = entities[i]; if (!entity) { continue; } if (typeof entity === 'string') { const lastResult = result.length > 0 && result[result.length - 1]; if (typeof lastResult === 'string') { result[result.length - 1] = lastResult + entity; } else { result.push(entity); } } else if (Array.isArray(entity)) { const [firstEntity, ...restOfEntity] = entity; const lastResult = result.length > 0 && result[result.length - 1]; if (typeof lastResult === 'string' && typeof firstEntity === 'string') { result[result.length - 1] = lastResult + firstEntity; } else if (firstEntity) { result.push(firstEntity); } result.push(...restOfEntity); } else { result.push(entity); } } return result; }; // defaults to shortName type EntityTextThreadInput = | { +display: 'uiName', +threadInfo: ThreadInfo, } | { +display?: 'shortName', +threadInfo: RawThreadInfo | ThreadInfo, +subchannel?: ?boolean, +possessive?: ?boolean, } | { +display: 'alwaysDisplayShortName', +threadInfo: RawThreadInfo | ThreadInfo, +possessive?: ?boolean, } | { +display: 'alwaysDisplayShortName', +threadID: string, +parentThreadID?: ?string, +threadType?: ?ThreadType, +possessive?: ?boolean, }; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return entityTextFunction.thread = (input: EntityTextThreadInput) => { if (input.display === 'uiName') { const { threadInfo } = input; if (typeof threadInfo.uiName !== 'string') { return threadInfo.uiName; } return { type: 'thread', id: threadInfo.id, name: threadInfo.name, display: 'uiName', uiName: threadInfo.uiName, }; } if (input.display === 'alwaysDisplayShortName' && input.threadID) { const { threadID, threadType, parentThreadID, possessive } = input; return { type: 'thread', id: threadID, name: undefined, display: 'shortName', threadType, parentThreadID, alwaysDisplayShortName: true, possessive, }; } else if (input.display === 'alwaysDisplayShortName' && input.threadInfo) { const { threadInfo, possessive } = input; return { type: 'thread', id: threadInfo.id, name: threadInfo.name, display: 'shortName', threadType: threadInfo.type, parentThreadID: threadInfo.parentThreadID, alwaysDisplayShortName: true, possessive, }; } else if (input.display === 'shortName' || !input.display) { const { threadInfo, subchannel, possessive } = input; return { type: 'thread', id: threadInfo.id, name: threadInfo.name, display: 'shortName', threadType: threadInfo.type, parentThreadID: threadInfo.parentThreadID, subchannel, possessive, }; } invariant( false, `ET.thread passed unexpected display type: ${input.display}`, ); }; type EntityTextUserInput = { +userInfo: { +id: string, +username?: ?string, +isViewer?: ?boolean, ... }, +possessive?: ?boolean, }; entityTextFunction.user = (input: EntityTextUserInput) => ({ type: 'user', id: input.userInfo.id, username: input.userInfo.username, isViewer: input.userInfo.isViewer, possessive: input.possessive, }); type EntityTextColorInput = { +hex: string }; entityTextFunction.color = (input: EntityTextColorInput) => ({ type: 'color', hex: input.hex, }); type EntityTextFarcasterUserInput = { +fid: string }; entityTextFunction.fcUser = (input: EntityTextFarcasterUserInput) => ({ type: 'farcaster_user', fid: input.fid, }); // ET is a JS tag function used in template literals, eg. ET`something` // It allows you to compose raw text and "entities" together type EntityTextFunction = (( strings: $ReadOnlyArray, ...entities: $ReadOnlyArray ) => EntityText) & { +thread: EntityTextThreadInput => ThreadEntity, +user: EntityTextUserInput => UserEntity, +color: EntityTextColorInput => ColorEntity, +fcUser: EntityTextFarcasterUserInput => FarcasterUserEntity, ... }; const ET: EntityTextFunction = entityTextFunction; type MakePossessiveInput = { +str: string, +isViewer?: ?boolean }; function makePossessive(input: MakePossessiveInput) { if (input.isViewer) { return 'your'; } return `${input.str}’s`; } function getNameForThreadEntity( entity: ThreadEntity, params?: ?EntityTextToRawStringParams, ): string { const { name: userGeneratedName, display } = entity; if (entity.display === 'uiName') { if (userGeneratedName) { return userGeneratedName; } const { uiName } = entity; if (typeof uiName === 'string') { return uiName; } let userEntities = uiName; if (!params?.ignoreViewer) { const viewerFilteredUserEntities = userEntities.filter( innerEntity => !innerEntity.isViewer, ); if (viewerFilteredUserEntities.length > 0) { userEntities = viewerFilteredUserEntities; } else if (entity.ifJustViewer === 'viewer_username') { // We pass ignoreViewer to entityTextToRawString in order // to prevent it from rendering the viewer as "you" params = { ...params, ignoreViewer: true }; } else { return 'just you'; } } const pluralized = pluralizeEntityText( userEntities.map(innerEntity => [innerEntity]), ); return entityTextToRawString(pluralized, params); } invariant( entity.display === 'shortName', `getNameForThreadEntity can't handle thread entity display ${display}`, ); let { name } = entity; if (!name || entity.alwaysDisplayShortName) { const threadType = entity.threadType ?? threadTypes.PERSONAL; const { parentThreadID } = entity; const noun = entity.subchannel ? 'subchannel' : threadNoun(threadType, parentThreadID); if (entity.id === params?.threadID) { const prefixThisThreadNounWith = params?.prefixThisThreadNounWith === 'your' ? 'your' : 'this'; name = `${prefixThisThreadNounWith} ${noun}`; } else { name = `a ${noun}`; } } if (entity.possessive) { name = makePossessive({ str: name }); } return name; } function getNameForUserEntity( entity: UserEntity, ignoreViewer: ?boolean, ): string { const isViewer = entity.isViewer && !ignoreViewer; const entityWithIsViewerIgnored = { ...entity, isViewer }; const str = stringForUser(entityWithIsViewerIgnored); if (!entityWithIsViewerIgnored.possessive) { return str; } return makePossessive({ str, isViewer }); } function getNameForFarcasterUserEntity(entity: FarcasterUserEntity): string { return entity.farcasterUsername ?? ''; } type EntityTextToRawStringParams = { +threadID?: ?string, +ignoreViewer?: ?boolean, +prefixThisThreadNounWith?: ?('this' | 'your'), }; function entityTextToRawString( entityText: EntityText, params?: ?EntityTextToRawStringParams, ): string { // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return const textParts = entityText.map(entity => { if (typeof entity === 'string') { return entity; } else if (entity.type === 'thread') { return getNameForThreadEntity(entity, params); } else if (entity.type === 'color') { return entity.hex; } else if (entity.type === 'user') { return getNameForUserEntity(entity, params?.ignoreViewer); } else if (entity.type === 'farcaster_user') { return getNameForFarcasterUserEntity(entity); } else { invariant( false, `entityTextToRawString can't handle entity type ${entity.type}`, ); } }); return textParts.join(''); } type RenderFunctions = { +renderText: ({ +text: string }) => React.Node, +renderThread: ({ +id: string, +name: string }) => React.Node, +renderUser: ({ +userID: string, +usernameText: string }) => React.Node, +renderColor: ({ +hex: string }) => React.Node, +renderFarcasterUser: ({ +farcasterUsername: string }) => React.Node, }; function entityTextToReact( entityText: EntityText, threadID: string, renderFuncs: RenderFunctions, ): React.Node { const { renderText, renderThread, renderUser, renderColor, renderFarcasterUser, } = renderFuncs; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return return entityText.map((entity, i) => { const key = `text${i}`; if (typeof entity === 'string') { return ( {renderText({ text: entity })} ); } else if (entity.type === 'thread') { const { id } = entity; const name = getNameForThreadEntity(entity, { threadID }); if (id === threadID) { return name; } else { return ( {renderThread({ id, name })} ); } } else if (entity.type === 'color') { return ( {renderColor({ hex: entity.hex })} ); } else if (entity.type === 'user') { const userID = entity.id; const usernameText = getNameForUserEntity(entity); return ( {renderUser({ userID, usernameText })} ); } else if (entity.type === 'farcaster_user') { const name = getNameForFarcasterUserEntity(entity); return ( {renderFarcasterUser({ farcasterUsername: name })} ); } else { invariant( false, `entityTextToReact can't handle entity type ${entity.type}`, ); } }); } function pluralizeEntityText( nouns: $ReadOnlyArray, maxNumberOfNouns: number = 3, ): EntityText { return basePluralize( nouns, maxNumberOfNouns, (a: EntityText | string, b: ?EntityText | string) => b ? ET`${a}${b}` : ET`${a}`, ); } type TextEntity = { +type: 'text', +text: string }; type ShadowUserEntity = { +type: 'shadowUser', +username: string, +originalUsername: string, }; type EntityTextComponentAsObject = | UserEntity | ThreadEntity | ColorEntity | TextEntity | FarcasterUserEntity | ShadowUserEntity; function entityTextToObjects( entityText: EntityText, ): EntityTextComponentAsObject[] { const objs: EntityTextComponentAsObject[] = []; for (const entity of entityText) { if (typeof entity === 'string') { objs.push({ type: 'text', text: entity }); continue; } objs.push(entity); if ( entity.type === 'thread' && entity.display === 'uiName' && typeof entity.uiName !== 'string' ) { for (const innerEntity of entity.uiName) { if (typeof innerEntity === 'string' || innerEntity.type !== 'user') { continue; } const { username } = innerEntity; if (username) { objs.push({ type: 'shadowUser', originalUsername: username, username, }); } } } } return objs; } function entityTextFromObjects( objects: $ReadOnlyArray, ): EntityText { const shadowUserMap = new Map(); for (const obj of objects) { if (obj.type === 'shadowUser' && obj.username !== obj.originalUsername) { shadowUserMap.set(obj.originalUsername, obj.username); } } return objects .map(entity => { if (entity.type === 'text') { return entity.text; } else if (entity.type === 'shadowUser') { return null; } else if ( entity.type === 'thread' && entity.display === 'uiName' && typeof entity.uiName !== 'string' ) { const uiName: UserEntity[] = []; let changeOccurred = false; for (const innerEntity of entity.uiName) { if (typeof innerEntity === 'string' || innerEntity.type !== 'user') { uiName.push(innerEntity); continue; } const { username } = innerEntity; if (!username) { uiName.push(innerEntity); continue; } const ensName = shadowUserMap.get(username); if (!ensName) { uiName.push(innerEntity); continue; } changeOccurred = true; uiName.push({ ...innerEntity, username: ensName, }); } if (!changeOccurred) { return entity; } return { ...entity, uiName, }; } else { return entity; } }) .filter(Boolean); } export type UseResolvedEntityTextOptions = { +allAtOnce?: ?boolean, }; function useResolvedEntityText( entityText: ?EntityText, options?: ?UseResolvedEntityTextOptions, ): ?EntityText { const allObjects = React.useMemo( () => (entityText ? entityTextToObjects(entityText) : []), [entityText], ); const objectsWithENSNames = useENSNames(allObjects, options); const objectsWithFCNames = useFCNames(allObjects, options); return React.useMemo(() => { if (!entityText) { return entityText; } const mergedObjects = []; for (let i = 0; i < allObjects.length; i++) { const originalObject = allObjects[i]; const updatedObject = originalObject.fid ? objectsWithFCNames[i] : objectsWithENSNames[i]; mergedObjects.push(updatedObject); } return entityTextFromObjects(mergedObjects); }, [entityText, allObjects, objectsWithENSNames, objectsWithFCNames]); } function useEntityTextAsString( entityText: ?EntityText, params?: EntityTextToRawStringParams, ): ?string { const withENSNames = useResolvedEntityText(entityText); return React.useMemo(() => { if (!withENSNames) { return withENSNames; } return entityTextToRawString(withENSNames, params); }, [withENSNames, params]); } type Fetchers = { +getENSNames?: ?GetENSNames, +getFCNames?: ?GetFCNames, }; async function getEntityTextAsString( entityText: ?EntityText, fetchers?: Fetchers, params?: EntityTextToRawStringParams, ): Promise { if (!entityText) { return entityText; } const getENSNames = fetchers?.getENSNames; const getFCNames = fetchers?.getFCNames; if (!getENSNames && !getFCNames) { return entityTextToRawString(entityText, params); } const allObjects = entityTextToObjects(entityText); const objectsWithENSNamesPromise = (async () => { if (!getENSNames) { return allObjects; } return await getENSNames(allObjects); })(); const objectsWithFCNamesPromise = (async () => { if (!getFCNames) { return allObjects; } return await getFCNames(allObjects); })(); const [objectsWithENSNames, objectsWithFCNames] = await Promise.all([ objectsWithENSNamesPromise, objectsWithFCNamesPromise, ]); const mergedObjects = []; for (let i = 0; i < allObjects.length; i++) { const originalObject = allObjects[i]; const updatedObject = originalObject.fid ? objectsWithFCNames[i] : objectsWithENSNames[i]; mergedObjects.push(updatedObject); } const resolvedEntityText = entityTextFromObjects(mergedObjects); return entityTextToRawString(resolvedEntityText, params); } export { ET, entityTextToRawString, entityTextToReact, getNameForThreadEntity, pluralizeEntityText, useResolvedEntityText, useEntityTextAsString, getEntityTextAsString, }; diff --git a/lib/utils/url-utils.js b/lib/utils/url-utils.js index b8a46749d..6bcab4c19 100644 --- a/lib/utils/url-utils.js +++ b/lib/utils/url-utils.js @@ -1,164 +1,165 @@ // @flow import t, { type TInterface } from 'tcomb'; import { idSchemaRegex, tID, tShape, pendingThreadIDRegex, + tUserID, } from './validation-utils.js'; type MutableURLInfo = { year?: number, month?: number, // 1-indexed verify?: string, calendar?: boolean, chat?: boolean, thread?: string, settings?: | 'account' | 'friend-list' | 'block-list' | 'keyservers' | 'build-info' | 'danger-zone', threadCreation?: boolean, selectedUserList?: $ReadOnlyArray, inviteSecret?: string, qrCode?: boolean, ... }; export type URLInfo = $ReadOnly; export const urlInfoValidator: TInterface = tShape({ year: t.maybe(t.Number), month: t.maybe(t.Number), verify: t.maybe(t.String), calendar: t.maybe(t.Boolean), chat: t.maybe(t.Boolean), thread: t.maybe(tID), settings: t.maybe( t.enums.of([ 'account', 'friend-list', 'block-list', 'keyservers', 'build-info', 'danger-zone', ]), ), threadCreation: t.maybe(t.Boolean), - selectedUserList: t.maybe(t.list(t.String)), + selectedUserList: t.maybe(t.list(tUserID)), inviteSecret: t.maybe(t.String), qrCode: t.maybe(t.Boolean), }); // We use groups to capture parts of the URL and any changes // to regexes must be reflected in infoFromURL. const yearRegex = new RegExp('(/|^)year/([0-9]+)(/|$)', 'i'); const monthRegex = new RegExp('(/|^)month/([0-9]+)(/|$)', 'i'); const threadRegex = new RegExp(`(/|^)thread/(${idSchemaRegex})(/|$)`, 'i'); const verifyRegex = new RegExp('(/|^)verify/([a-f0-9]+)(/|$)', 'i'); const calendarRegex = new RegExp('(/|^)calendar(/|$)', 'i'); const chatRegex = new RegExp('(/|^)chat(/|$)', 'i'); const accountSettingsRegex = new RegExp('(/|^)settings/account(/|$)', 'i'); const friendListRegex = new RegExp('(/|^)settings/friend-list(/|$)', 'i'); const blockListRegex = new RegExp('(/|^)settings/block-list(/|$)', 'i'); const keyserversRegex = new RegExp('(/|^)settings/keyservers(/|$)', 'i'); const buildInfoRegex = new RegExp('(/|^)settings/build-info(/|$)', 'i'); const dangerZoneRegex = new RegExp('(/|^)settings/danger-zone(/|$)', 'i'); const threadPendingRegex = new RegExp( `(/|^)thread/(${pendingThreadIDRegex})(/|$)`, 'i', ); const threadCreationRegex = new RegExp( '(/|^)thread/new(/([0-9]+([+][0-9]+)*))?(/|$)', 'i', ); const inviteLinkRegex = new RegExp( '(/|^)handle/invite/([a-zA-Z0-9]+)(/|$)', 'i', ); const qrCodeLoginRegex = new RegExp('(/|^)qr-code(/|$)', 'i'); function infoFromURL(url: string): URLInfo { const yearMatches = yearRegex.exec(url); const monthMatches = monthRegex.exec(url); const threadMatches = threadRegex.exec(url); const verifyMatches = verifyRegex.exec(url); const calendarTest = calendarRegex.test(url); const chatTest = chatRegex.test(url); const accountSettingsTest = accountSettingsRegex.test(url); const friendListTest = friendListRegex.test(url); const blockListTest = blockListRegex.test(url); const keyserversSettingsTest = keyserversRegex.test(url); const buildInfoTest = buildInfoRegex.test(url); const dangerZoneTest = dangerZoneRegex.test(url); const threadPendingMatches = threadPendingRegex.exec(url); const threadCreateMatches = threadCreationRegex.exec(url); const inviteLinkMatches = inviteLinkRegex.exec(url); const qrCodeLoginMatches = qrCodeLoginRegex.exec(url); const returnObj: MutableURLInfo = {}; if (yearMatches) { returnObj.year = parseInt(yearMatches[2], 10); } if (monthMatches) { const month = parseInt(monthMatches[2], 10); if (month < 1 || month > 12) { throw new Error('invalid_month'); } returnObj.month = month; } if (threadMatches) { returnObj.thread = threadMatches[2]; } if (threadPendingMatches) { returnObj.thread = threadPendingMatches[2]; } if (threadCreateMatches) { returnObj.threadCreation = true; returnObj.selectedUserList = threadCreateMatches[3]?.split('+') ?? []; } if (verifyMatches) { returnObj.verify = verifyMatches[2]; } if (inviteLinkMatches) { returnObj.inviteSecret = inviteLinkMatches[2]; } if (calendarTest) { returnObj.calendar = true; } else if (chatTest) { returnObj.chat = true; } else if (accountSettingsTest) { returnObj.settings = 'account'; } else if (friendListTest) { returnObj.settings = 'friend-list'; } else if (blockListTest) { returnObj.settings = 'block-list'; } else if (keyserversSettingsTest) { returnObj.settings = 'keyservers'; } else if (buildInfoTest) { returnObj.settings = 'build-info'; } else if (dangerZoneTest) { returnObj.settings = 'danger-zone'; } else if (qrCodeLoginMatches) { returnObj.qrCode = true; } return returnObj; } const setURLPrefix = 'SET_URL_PREFIX'; export type URLPathParams = { +[name: string]: string }; function replacePathParams(path: string, params: URLPathParams = {}): string { for (const name in params) { path = path.replace(`:${name}`, params[name]); } return path; } export { infoFromURL, setURLPrefix, replacePathParams };