diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -10,6 +10,7 @@ UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, + LogInResponse, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, @@ -31,12 +32,17 @@ SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; +import type { RawThreadInfo } from '../types/thread-types'; import type { UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; -import { extractKeyserverIDFromID } from '../utils/action-utils.js'; +import { + extractKeyserverIDFromID, + sortThreadIDsPerKeyserver, + sortCalendarQueryPerKeyserver, +} from '../utils/action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, @@ -197,50 +203,108 @@ const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( - callServerEndpoint: CallServerEndpoint, - ): ((logInInfo: LogInInfo) => Promise) => + callKeyserverEndpoint: CallKeyserverEndpoint, + ): ((input: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); - const { logInActionSource, ...restLogInInfo } = logInInfo; - - const deviceTokenUpdateRequest = - logInInfo.deviceTokenUpdateRequest[ashoatKeyserverID]; + 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 response = await callServerEndpoint( - 'log_in', - { + const requests = {}; + for (const keyserverID of keyserverIDs) { + requests[keyserverID] = { ...restLogInInfo, - deviceTokenUpdateRequest, + deviceTokenUpdateRequest: + logInInfo.deviceTokenUpdateRequest[keyserverID], source: logInActionSource, - watchedIDs, + watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], + calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, - }, + }; + } + + const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( + 'log_in', + requests, logInCallServerEndpointOptions, ); - const userInfos = mergeUserInfos( - response.userInfos, - response.cookieChange.userInfos, - ); + + const userInfosArrays = []; + + let threadInfos: { +[id: string]: RawThreadInfo } = {}; + const calendarResult = { + calendarQuery: logInInfo.calendarQuery, + rawEntryInfos: [], + }; + const messagesResult = { + 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: response.cookieChange.threadInfos, - currentUserInfo: response.currentUserInfo, - calendarResult: { - calendarQuery: logInInfo.calendarQuery, - rawEntryInfos: response.rawEntryInfos, - }, - messagesResult: { - messageInfos: response.rawMessageInfos, - truncationStatus: response.truncationStatuses, - watchedIDsAtRequestTime: watchedIDs, - currentAsOf: response.serverTime, - }, + threadInfos, + currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, + calendarResult, + messagesResult, userInfos, - updatesCurrentAsOf: response.serverTime, + updatesCurrentAsOf, logInActionSource: logInInfo.logInActionSource, - notAcknowledgedPolicies: response.notAcknowledgedPolicies, + 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', @@ -422,7 +486,8 @@ getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, - logIn, + logIn as logInRawAction, + useLogIn, logInActionTypes, useLogOut, logOutActionTypes, diff --git a/lib/reducers/keyserver-reducer.js b/lib/reducers/keyserver-reducer.js --- a/lib/reducers/keyserver-reducer.js +++ b/lib/reducers/keyserver-reducer.js @@ -26,24 +26,19 @@ ): KeyserverStore { // this action is only dispatched on native if (action.type === resetUserStateActionType) { - const stateCookie = state.keyserverInfos[ashoatKeyserverID]?.cookie; - const cookie = - stateCookie && stateCookie.startsWith('anonymous=') ? stateCookie : null; - const keyserverInfos = { ...state.keyserverInfos }; - for (const key in keyserverInfos) { - keyserverInfos[key] = { ...keyserverInfos[key], cookie: null }; + for (const keyserverID in keyserverInfos) { + const stateCookie = state.keyserverInfos[keyserverID]?.cookie; + const cookie = + stateCookie && stateCookie.startsWith('anonymous=') + ? stateCookie + : null; + keyserverInfos[keyserverID] = { ...keyserverInfos[keyserverID], cookie }; } state = { ...state, - keyserverInfos: { - ...keyserverInfos, - [ashoatKeyserverID]: { - ...state.keyserverInfos[ashoatKeyserverID], - cookie, - }, - }, + keyserverInfos, }; } else if (action.type === setNewSessionActionType) { if (action.payload.sessionChange.cookie !== undefined) { diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js --- a/lib/reducers/message-reducer.js +++ b/lib/reducers/message-reducer.js @@ -753,7 +753,7 @@ freshMessageStore( messagesResult.messageInfos, messagesResult.truncationStatus, - messagesResult.currentAsOf, + messagesResult.currentAsOf[ashoatKeyserverID], newThreadInfos, ); diff --git a/lib/reducers/updates-reducer.js b/lib/reducers/updates-reducer.js --- a/lib/reducers/updates-reducer.js +++ b/lib/reducers/updates-reducer.js @@ -8,6 +8,7 @@ incrementalStateSyncActionType, } from '../types/socket-types.js'; import { processUpdatesActionType } from '../types/update-types.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; function reduceUpdatesCurrentAsOf( currentAsOf: number, @@ -17,7 +18,7 @@ action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success ) { - return action.payload.updatesCurrentAsOf; + return action.payload.updatesCurrentAsOf[ashoatKeyserverID]; } else if (action.type === fullStateSyncActionType) { return action.payload.updatesCurrentAsOf; } else if (action.type === incrementalStateSyncActionType) { diff --git a/lib/socket/socket.react.js b/lib/socket/socket.react.js --- a/lib/socket/socket.react.js +++ b/lib/socket/socket.react.js @@ -73,6 +73,7 @@ import { ServerError, SocketTimeout, SocketOffline } from '../utils/errors.js'; import { promiseAll } from '../utils/promises.js'; import sleep from '../utils/sleep.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; const remainingTimeAfterVisualTimeout = clientRequestSocketTimeout - clientRequestVisualTimeout; @@ -483,6 +484,7 @@ cookie, this.props.urlPrefix, logInActionSources.socketAuthErrorResolutionAttempt, + ashoatKeyserverID, this.props.getInitialNotificationsEncryptedMessage, ); diff --git a/lib/types/account-types.js b/lib/types/account-types.js --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -117,6 +117,7 @@ +username: string, +password: string, +logInActionSource: LogInActionSource, + +keyserverIDs?: $ReadOnlyArray, }; export type LogInRequest = { @@ -153,7 +154,7 @@ +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, - +updatesCurrentAsOf: number, + +updatesCurrentAsOf: { +[keyserverID: string]: number }, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -644,7 +644,7 @@ +messageInfos: RawMessageInfo[], +truncationStatus: MessageTruncationStatuses, +watchedIDsAtRequestTime: $ReadOnlyArray, - +currentAsOf: number, + +currentAsOf: { +[keyserverID: string]: number }, }; export type SaveMessagesPayload = { diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -12,6 +12,8 @@ CallServerEndpointOptions, } from './call-server-endpoint.js'; import { getConfig } from './config.js'; +import { promiseAll } from './promises.js'; +import { ashoatKeyserverID } from './validation-utils.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; import { logInActionSources, @@ -186,6 +188,7 @@ cookie: ?string, urlPrefix: string, logInActionSource: LogInActionSource, + keyserverID: string, getInitialNotificationsEncryptedMessage?: () => Promise, ): Promise { const resolveInvalidatedCookie = getConfig().resolveInvalidatedCookie; @@ -196,7 +199,7 @@ let callServerEndpointCallback = null; const boundCallServerEndpoint = async ( endpoint: Endpoint, - data: { [key: string]: mixed }, + data: { +[key: string]: mixed }, options?: ?CallServerEndpointOptions, ) => { const innerBoundSetNewSession = ( @@ -234,6 +237,25 @@ throw e; } }; + + const boundCallKeyserverEndpoint = ( + endpoint: Endpoint, + requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, + options?: ?CallServerEndpointOptions, + ) => { + if (requests[keyserverID]) { + const promises = { + [keyserverID]: boundCallServerEndpoint( + endpoint, + requests[keyserverID], + options, + ), + }; + return promiseAll(promises); + } + return Promise.resolve({}); + }; + const dispatchRecoveryAttempt = ( actionTypes: ActionTypes< 'LOG_IN_STARTED', @@ -249,8 +271,10 @@ }; await resolveInvalidatedCookie( boundCallServerEndpoint, + boundCallKeyserverEndpoint, dispatchRecoveryAttempt, logInActionSource, + keyserverID, getInitialNotificationsEncryptedMessage, ); return newSessionChange; @@ -269,6 +293,7 @@ currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, + keyserverID, } = params; const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); const boundSetNewSession = ( @@ -308,6 +333,7 @@ newAnonymousCookie, urlPrefix, logInActionSources.cookieInvalidationResolutionAttempt, + keyserverID, ); currentlyWaitingForNewCookie = false; @@ -381,6 +407,7 @@ +currentUserInfo: ?CurrentUserInfo, +connectionStatus: ConnectionStatus, +lastCommunicatedPlatformDetails: ?PlatformDetails, + +keyserverID: string, }; // All server calls needs to include some information from the Redux state @@ -403,6 +430,7 @@ (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.connectionStatus, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, + (state: BindServerCallsParams) => state.keyserverID, ( dispatch: Dispatch, cookie: ?string, @@ -411,6 +439,7 @@ currentUserInfo: ?CurrentUserInfo, connectionStatus: ConnectionStatus, lastCommunicatedPlatformDetails: ?PlatformDetails, + keyserverID: string, ) => { const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, @@ -420,6 +449,7 @@ currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, + keyserverID, }); return actionFunc(boundCallServerEndpoint); }, @@ -450,6 +480,7 @@ connectionStatus, dispatch, ...paramOverride, + keyserverID: ashoatKeyserverID, }); }, [serverCall, serverCallState, dispatch, paramOverride]); } diff --git a/lib/utils/call-server-endpoint.js b/lib/utils/call-server-endpoint.js --- a/lib/utils/call-server-endpoint.js +++ b/lib/utils/call-server-endpoint.js @@ -69,7 +69,7 @@ ) => Promise; type RequestData = { - input: { [key: string]: mixed }, + input: { +[key: string]: mixed }, cookie?: ?string, sessionID?: ?string, platformDetails?: PlatformDetails, @@ -88,7 +88,7 @@ lastCommunicatedPlatformDetails: ?PlatformDetails, socketAPIHandler: ?SocketAPIHandler, endpoint: Endpoint, - input: { [key: string]: mixed }, + input: { +[key: string]: mixed }, dispatch: Dispatch, options?: ?CallServerEndpointOptions, loggedIn: boolean, diff --git a/lib/utils/config.js b/lib/utils/config.js --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -4,14 +4,17 @@ import type { DispatchRecoveryAttempt } from './action-utils.js'; import type { CallServerEndpoint } from './call-server-endpoint.js'; +import type { CallKeyserverEndpoint } from './keyserver-call.js'; import type { LogInActionSource } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; export type Config = { +resolveInvalidatedCookie: ?( callServerEndpoint: CallServerEndpoint, + callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, + keyserverID: string, getInitialNotificationsEncryptedMessage?: () => Promise, ) => Promise, +setSessionIDOnRequest: boolean, diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -65,6 +65,7 @@ currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, + keyserverID, }), ); @@ -119,6 +120,7 @@ sessionID, connectionStatus: connection?.status, lastCommunicatedPlatformDetails, + keyserverID, }); return boundCallServerEndpoint( diff --git a/lib/utils/upload-blob.js b/lib/utils/upload-blob.js --- a/lib/utils/upload-blob.js +++ b/lib/utils/upload-blob.js @@ -13,7 +13,7 @@ url: string, cookie: ?string, sessionID: ?string, - input: { [key: string]: mixed }, + input: { +[key: string]: mixed }, options?: ?CallServerEndpointOptions, ): Promise { const formData = new FormData(); diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -7,7 +7,7 @@ import { logInActionTypes, - logIn, + useLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { @@ -27,7 +27,6 @@ } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { - useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; @@ -386,7 +385,7 @@ ); const dispatchActionPromise = useDispatchActionPromise(); - const callLogIn = useServerCall(logIn); + const callLogIn = useLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(); diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -27,6 +27,7 @@ import { logInActionSources } from 'lib/types/account-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-panel.react.js'; @@ -324,6 +325,7 @@ cookie, urlPrefix, actionSource, + ashoatKeyserverID, this.props.getInitialNotificationsEncryptedMessage, ); if ( diff --git a/native/account/resolve-invalidated-cookie.js b/native/account/resolve-invalidated-cookie.js --- a/native/account/resolve-invalidated-cookie.js +++ b/native/account/resolve-invalidated-cookie.js @@ -1,9 +1,10 @@ // @flow -import { logInActionTypes, logIn } from 'lib/actions/user-actions.js'; +import { logInActionTypes, logInRawAction } from 'lib/actions/user-actions.js'; import type { LogInActionSource } from 'lib/types/account-types.js'; import type { DispatchRecoveryAttempt } from 'lib/utils/action-utils.js'; import type { CallServerEndpoint } from 'lib/utils/call-server-endpoint.js'; +import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; @@ -13,8 +14,10 @@ async function resolveInvalidatedCookie( callServerEndpoint: CallServerEndpoint, + callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, + keyserverID: string, getInitialNotificationsEncryptedMessage?: ( ?InitialNotifMessageOptions, ) => Promise, @@ -37,10 +40,11 @@ const { calendarQuery } = extraInfo; await dispatchRecoveryAttempt( logInActionTypes, - logIn(callServerEndpoint)({ + logInRawAction(callKeyserverEndpoint)({ ...keychainCredentials, ...extraInfo, logInActionSource, + keyserverIDs: [keyserverID], }), { calendarQuery }, ); diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -19,6 +19,7 @@ } from 'lib/types/account-types.js'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; @@ -67,6 +68,7 @@ cookie, urlPrefix, source, + ashoatKeyserverID, getInitialNotificationsEncryptedMessage, ); dispatch({ type: setStoreLoadedActionType }); diff --git a/native/input/input-state-container.react.js b/native/input/input-state-container.react.js --- a/native/input/input-state-container.react.js +++ b/native/input/input-state-container.react.js @@ -1157,7 +1157,7 @@ url: string, cookie: ?string, sessionID: ?string, - input: { [key: string]: mixed }, + input: { +[key: string]: mixed }, options?: ?CallServerEndpointOptions, ): Promise => { invariant( diff --git a/native/socket.react.js b/native/socket.react.js --- a/native/socket.react.js +++ b/native/socket.react.js @@ -20,6 +20,7 @@ useDispatchActionPromise, fetchNewCookieFromNativeCredentials, } from 'lib/utils/action-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { InputStateContext } from './input/input-state.js'; import { @@ -127,6 +128,7 @@ cookie, urlPrefix, logInActionSources.refetchUserDataAfterAcknowledgment, + ashoatKeyserverID, getInitialNotificationsEncryptedMessage, ); }, [ diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -3,7 +3,7 @@ import invariant from 'invariant'; import * as React from 'react'; -import { logIn, logInActionTypes } from 'lib/actions/user-actions.js'; +import { useLogIn, logInActionTypes } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { @@ -16,10 +16,7 @@ } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; -import { - useDispatchActionPromise, - useServerCall, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { useSignedIdentityKeysBlob } from './account-hooks.js'; import HeaderSeparator from './header-separator.react.js'; @@ -35,7 +32,7 @@ function TraditionalLoginForm(): React.Node { const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; const loginExtraInfo = useSelector(webLogInExtraInfoSelector); - const callLogIn = useServerCall(logIn); + const callLogIn = useLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext();