diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index c8b81f055..019287a13 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,567 +1,687 @@ // @flow import * as React from 'react'; import { preRequestUserStateSelector } from '../selectors/account-selectors.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInResponse, LogInRequest, + KeyserverAuthResult, + KeyserverAuthInfo, + KeyserverAuthRequest, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; import type { IdentityServiceClient } from '../types/identity-service-types'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { GetSessionPublicKeysArgs, GetOlmSessionInitializationDataResponse, } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../utils/action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import { useSelector } from '../utils/redux-utils.js'; import sleep from '../utils/sleep.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState }; }; function useLogOut(): () => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverLogOut = useKeyserverCall(logOut); return React.useCallback( () => callKeyserverLogOut(preRequestUserState), [callKeyserverLogOut, preRequestUserState], ); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallServerEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [ashoatKeyserverID]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallServerEndpointOptions, }); const response = responses[ashoatKeyserverID]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): () => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState }; }; function useDeleteKeyserverAccount(): ( input: PreRequestUserState, ) => Promise { return useKeyserverCall(deleteKeyserverAccount); } const deleteIdentityAccountActionTypes = Object.freeze({ started: 'DELETE_IDENTITY_ACCOUNT_STARTED', success: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', failed: 'DELETE_IDENTITY_ACCOUNT_FAILED', }); function useDeleteIdentityAccount(): ( client: IdentityServiceClient, deviceID: ?string, ) => Promise { const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const deleteIdentityAccount = React.useCallback( async (client: IdentityServiceClient, deviceID: ?string) => { if (!userID || !accessToken || !deviceID) { throw new Error('missing identity service auth metadata'); } await client.deleteUser(userID, deviceID, accessToken); }, [userID, accessToken], ); return deleteIdentityAccount; } const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, ): (( registerInfo: RegisterInfo, options?: CallServerEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[ashoatKeyserverID]; const response = await callServerEndpoint( 'create_account', { ...registerInfo, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...registerCallServerEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; +const keyserverAuthActionTypes = Object.freeze({ + started: 'KEYSERVER_AUTH_STARTED', + success: 'KEYSERVER_AUTH_SUCCESS', + failed: 'KEYSERVER_AUTH_FAILED', +}); +const keyserverAuthCallServerEndpointOptions = { timeout: 60000 }; +const keyserverAuth = + ( + callKeyserverEndpoint: CallKeyserverEndpoint, + ): ((input: KeyserverAuthInfo) => Promise) => + async keyserverAuthInfo => { + const watchedIDs = threadWatcher.getWatchedIDs(); + + const { + logInActionSource, + calendarQuery, + keyserverData, + deviceTokenUpdateInput, + ...restLogInInfo + } = keyserverAuthInfo; + + const keyserverIDs = Object.keys(keyserverData); + + const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); + const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( + calendarQuery, + keyserverIDs, + ); + + const requests: { [string]: KeyserverAuthRequest } = {}; + for (const keyserverID of keyserverIDs) { + requests[keyserverID] = { + ...restLogInInfo, + deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], + watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], + calendarQuery: calendarQueryPerKeyserver[keyserverID], + platformDetails: getConfig().platformDetails, + initialContentEncryptedMessage: + keyserverData[keyserverID].initialContentEncryptedMessage, + initialNotificationsEncryptedMessage: + keyserverData[keyserverID].initialNotificationsEncryptedMessage, + source: logInActionSource, + }; + } + + const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( + 'keyserver_auth', + requests, + keyserverAuthCallServerEndpointOptions, + ); + + const userInfosArrays = []; + + let threadInfos: RawThreadInfos = {}; + const calendarResult: WritableCalendarResult = { + calendarQuery: keyserverAuthInfo.calendarQuery, + rawEntryInfos: [], + }; + const messagesResult: WritableGenericMessagesResult = { + messageInfos: [], + truncationStatus: {}, + watchedIDsAtRequestTime: watchedIDs, + currentAsOf: {}, + }; + let updatesCurrentAsOf: { +[string]: number } = {}; + for (const keyserverID in responses) { + threadInfos = { + ...responses[keyserverID].cookieChange.threadInfos, + ...threadInfos, + }; + if (responses[keyserverID].rawEntryInfos) { + calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( + responses[keyserverID].rawEntryInfos, + ); + } + messagesResult.messageInfos = messagesResult.messageInfos.concat( + responses[keyserverID].rawMessageInfos, + ); + messagesResult.truncationStatus = { + ...messagesResult.truncationStatus, + ...responses[keyserverID].truncationStatuses, + }; + messagesResult.currentAsOf = { + ...messagesResult.currentAsOf, + [keyserverID]: responses[keyserverID].serverTime, + }; + updatesCurrentAsOf = { + ...updatesCurrentAsOf, + [keyserverID]: responses[keyserverID].serverTime, + }; + userInfosArrays.push(responses[keyserverID].userInfos); + userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); + } + + const userInfos = mergeUserInfos(...userInfosArrays); + + return { + threadInfos, + currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, + calendarResult, + messagesResult, + userInfos, + updatesCurrentAsOf, + logInActionSource: keyserverAuthInfo.logInActionSource, + notAcknowledgedPolicies: + responses[ashoatKeyserverID].notAcknowledgedPolicies, + }; + }; + +function useKeyserverAuth(): ( + input: KeyserverAuthInfo, +) => Promise { + return useKeyserverCall(keyserverAuth); +} + function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [ashoatKeyserverID]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: logInActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( 'log_in', requests, logInCallServerEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: responses[ashoatKeyserverID].notAcknowledgedPolicies, }; }; function useLogIn(): (input: LogInInfo) => Promise { return useKeyserverCall(logIn); } const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callServerEndpoint: CallServerEndpoint, ): ((username: string) => Promise) => async username => { const response = await callServerEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callServerEndpoint: CallServerEndpoint, ): (( options?: ?CallServerEndpointOptions, ) => Promise) => async options => { return await callServerEndpoint( 'get_olm_session_initialization_data', {}, options, ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callServerEndpoint: CallServerEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callServerEndpoint( 'update_user_avatar', avatarDBContent, ); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeUserPasswordActionTypes, changeUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn as logInRawAction, useLogIn, logInActionTypes, useLogOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, deleteIdentityAccountActionTypes, useDeleteIdentityAccount, + keyserverAuthActionTypes, + useKeyserverAuth, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index 7d45f7eef..9bc130ece 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,210 +1,250 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import { type RawMessageInfo, type MessageTruncationStatuses, type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; import { type RawThreadInfos } from './thread-types.js'; import { type UserInfo, type LoggedOutUserInfo, type LoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, }; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type RegisterInfo = { ...LogInExtraInfo, +username: string, +password: string, }; export type DeviceTokenUpdateRequest = { +deviceToken: string, }; type DeviceTokenUpdateInput = { +[keyserverID: string]: DeviceTokenUpdateRequest, }; export type RegisterRequest = { +username: string, +email?: empty, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type RegisterResponse = { id: string, rawMessageInfos: $ReadOnlyArray, currentUserInfo: LoggedInUserInfo, cookieChange: { threadInfos: RawThreadInfos, userInfos: $ReadOnlyArray, }, }; export type RegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export const logInActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', sqliteOpFailure: 'SQLITE_OP_FAILURE', sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', + keyserverAuthFromNative: 'KEYSERVER_AUTH_FROM_NATIVE', + keyserverAuthFromWeb: 'KEYSERVER_AUTH_FROM_WEB', }); export type LogInActionSource = $Values; export type LogInStartingPayload = { +calendarQuery: CalendarQuery, +logInActionSource?: LogInActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest: DeviceTokenUpdateInput, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type LogInInfo = { ...LogInExtraInfo, +username: string, +password: string, +logInActionSource: LogInActionSource, +keyserverIDs?: $ReadOnlyArray, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: LogInActionSource, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type LogInResponse = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type LogInResult = { +threadInfos: RawThreadInfos, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; +export type KeyserverAuthResult = { + +threadInfos: RawThreadInfos, + +currentUserInfo?: ?LoggedInUserInfo, + +messagesResult: GenericMessagesResult, + +userInfos: $ReadOnlyArray, + +calendarResult: CalendarResult, + +updatesCurrentAsOf: { +[keyserverID: string]: number }, + +logInActionSource: LogInActionSource, + +notAcknowledgedPolicies?: ?$ReadOnlyArray, +}; + +type KeyserverRequestData = { + +initialContentEncryptedMessage: string, + +initialNotificationsEncryptedMessage: string, +}; + +export type KeyserverAuthInfo = { + +userID: string, + +deviceID: string, + +doNotRegister: boolean, + +calendarQuery: CalendarQuery, + +deviceTokenUpdateInput: DeviceTokenUpdateInput, + +logInActionSource: LogInActionSource, + +keyserverData: { +[keyserverID: string]: KeyserverRequestData }, +}; + +export type KeyserverAuthRequest = $ReadOnly<{ + ...KeyserverRequestData, + +userID: string, + +deviceID: string, + +doNotRegister: boolean, + +calendarQuery: CalendarQuery, + +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, + +watchedIDs: $ReadOnlyArray, + +platformDetails: PlatformDetails, + +source?: LogInActionSource, +}>; + export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const defaultNotificationPayloadValidator: TInterface = tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); export type ClaimUsernameResponse = { +message: string, +signature: string, }; diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js index 8f412a40c..d2a3fc4d1 100644 --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -1,138 +1,139 @@ // @flow export type APIRequest = { endpoint: Endpoint, input?: Object, }; export type SocketAPIHandler = (request: APIRequest) => Promise; export type Endpoint = | HTTPOnlyEndpoint | SocketOnlyEndpoint | HTTPPreferredEndpoint | SocketPreferredEndpoint; // Endpoints that can cause session changes should occur over HTTP, since the // socket code does not currently support changing sessions. In the future they // could be made to work for native, but cookie changes on web require HTTP // since websockets aren't able to Set-Cookie. Note that technically any // endpoint can cause a sessionChange, and in that case the server will close // the socket with a specific error code, and the client will proceed via HTTP. const sessionChangingEndpoints = Object.freeze({ LOG_OUT: 'log_out', DELETE_ACCOUNT: 'delete_account', CREATE_ACCOUNT: 'create_account', LOG_IN: 'log_in', UPDATE_PASSWORD: 'update_password', POLICY_ACKNOWLEDGMENT: 'policy_acknowledgment', + KEYSERVER_AUTH: 'keyserver_auth', }); type SessionChangingEndpoint = $Values; // We do uploads over HTTP as well. This is because Websockets use TCP, which // guarantees ordering. That means that if we start an upload, any messages we // try to send the server after the upload starts will have to wait until the // upload ends. To avoid blocking other messages we upload using HTTP // multipart/form-data. const uploadEndpoints = Object.freeze({ UPLOAD_MULTIMEDIA: 'upload_multimedia', }); type UploadEndpoint = $Values; const intialReduxStateEndpoints = Object.freeze({ GET_INITIAL_REDUX_STATE: 'get_initial_redux_state', }); type InitialReduxStateEndpoint = $Values; type HTTPOnlyEndpoint = | SessionChangingEndpoint | UploadEndpoint | InitialReduxStateEndpoint; const socketOnlyEndpoints = Object.freeze({ UPDATE_ACTIVITY: 'update_activity', UPDATE_CALENDAR_QUERY: 'update_calendar_query', }); type SocketOnlyEndpoint = $Values; const socketPreferredEndpoints = Object.freeze({}); type SocketPreferredEndpoint = $Values; const httpPreferredEndpoints = Object.freeze({ CREATE_REPORT: 'create_report', CREATE_REPORTS: 'create_reports', CREATE_ENTRY: 'create_entry', CREATE_ERROR_REPORT: 'create_error_report', CREATE_MESSAGE_REPORT: 'create_message_report', CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message', CREATE_OR_UPDATE_PUBLIC_LINK: 'create_or_update_public_link', CREATE_REACTION_MESSAGE: 'create_reaction_message', EDIT_MESSAGE: 'edit_message', CREATE_TEXT_MESSAGE: 'create_text_message', CREATE_THREAD: 'create_thread', DELETE_ENTRY: 'delete_entry', DELETE_COMMUNITY_ROLE: 'delete_community_role', DELETE_THREAD: 'delete_thread', DELETE_UPLOAD: 'delete_upload', DISABLE_INVITE_LINK: 'disable_invite_link', EXACT_SEARCH_USER: 'exact_search_user', FETCH_ENTRIES: 'fetch_entries', FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions', FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos', FETCH_MESSAGES: 'fetch_messages', FETCH_PINNED_MESSAGES: 'fetch_pinned_messages', FETCH_PRIMARY_INVITE_LINKS: 'fetch_primary_invite_links', FETCH_THREAD_MEDIA: 'fetch_thread_media', GET_SESSION_PUBLIC_KEYS: 'get_session_public_keys', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', MODIFY_COMMUNITY_ROLE: 'modify_community_role', REMOVE_MEMBERS: 'remove_members', REQUEST_ACCESS: 'request_access', RESTORE_ENTRY: 'restore_entry', SEARCH_USERS: 'search_users', SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email', SEND_VERIFICATION_EMAIL: 'send_verification_email', SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status', TOGGLE_MESSAGE_PIN: 'toggle_message_pin', UPDATE_ACCOUNT: 'update_account', UPDATE_USER_SETTINGS: 'update_user_settings', UPDATE_DEVICE_TOKEN: 'update_device_token', UPDATE_ENTRY: 'update_entry', UPDATE_RELATIONSHIPS: 'update_relationships', UPDATE_ROLE: 'update_role', UPDATE_THREAD: 'update_thread', UPDATE_USER_SUBSCRIPTION: 'update_user_subscription', VERIFY_CODE: 'verify_code', VERIFY_INVITE_LINK: 'verify_invite_link', SIWE_NONCE: 'siwe_nonce', SIWE_AUTH: 'siwe_auth', CLAIM_USERNAME: 'claim_username', UPDATE_USER_AVATAR: 'update_user_avatar', UPLOAD_MEDIA_METADATA: 'upload_media_metadata', SEARCH_MESSAGES: 'search_messages', GET_OLM_SESSION_INITIALIZATION_DATA: 'get_olm_session_initialization_data', VERSION: 'version', }); type HTTPPreferredEndpoint = $Values; const socketPreferredEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ]); export function endpointIsSocketPreferred(endpoint: Endpoint): boolean { return socketPreferredEndpointSet.has(endpoint); } const socketSafeEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ...Object.values(httpPreferredEndpoints), ]); export function endpointIsSocketSafe(endpoint: Endpoint): boolean { return socketSafeEndpointSet.has(endpoint); } const socketOnlyEndpointSet = new Set(Object.values(socketOnlyEndpoints)); export function endpointIsSocketOnly(endpoint: Endpoint): boolean { return socketOnlyEndpointSet.has(endpoint); }