diff --git a/lib/keyserver-conn/call-single-keyserver-endpoint.js b/lib/keyserver-conn/call-single-keyserver-endpoint.js index 031d37468..d92e95a3a 100644 --- a/lib/keyserver-conn/call-single-keyserver-endpoint.js +++ b/lib/keyserver-conn/call-single-keyserver-endpoint.js @@ -1,252 +1,252 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { defaultPerformHTTPMultipartUpload, type PerformHTTPMultipartUpload, } from './multipart-upload.js'; import { updateLastCommunicatedPlatformDetailsActionType } from '../actions/device-actions.js'; import { callSingleKeyserverEndpointTimeout } from '../shared/timeouts.js'; import type { PlatformDetails } from '../types/device-types.js'; import { type Endpoint, type SocketAPIHandler, endpointIsSocketPreferred, endpointIsSocketOnly, } from '../types/endpoints.js'; import { forcePolicyAcknowledgmentActionType } from '../types/policy-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ServerSessionChange, ClientSessionChange, } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; import { ServerError, FetchTimeout, SocketOffline, SocketTimeout, } from '../utils/errors.js'; import sleep from '../utils/sleep.js'; import { extractAndPersistUserInfosFromEndpointResponse } from '../utils/user-info-extraction-utils.js'; export type CallSingleKeyserverEndpointOptions = Partial<{ // null timeout means no timeout, which is the default for // defaultPerformHTTPMultipartUpload +timeout: ?number, // in milliseconds // getResultInfo will be called right before callSingleKeyserverEndpoint // successfully resolves and includes additional information about the request +getResultInfo: (resultInfo: CallSingleKeyserverEndpointResultInfo) => mixed, +performHTTPMultipartUpload: boolean | PerformHTTPMultipartUpload, // onProgress and abortHandler only work with performHTTPMultipartUpload +onProgress: (percent: number) => void, // abortHandler will receive an abort function once the upload starts +abortHandler: (abort: () => void) => void, // Overrides urlPrefix in Redux +urlPrefixOverride: string, }>; export type CallSingleKeyserverEndpointResultInfoInterface = 'socket' | 'REST'; export type CallSingleKeyserverEndpointResultInfo = { +interface: CallSingleKeyserverEndpointResultInfoInterface, }; export type CallSingleKeyserverEndpointResponse = Partial<{ +cookieChange: ServerSessionChange, +currentUserInfo: CurrentUserInfo, +error: string, +payload: Object, }>; // You'll notice that this is not the type of the callSingleKeyserverEndpoint // function below. This is because the first several parameters to that // function get bound in by the helpers in legacy-keyserver-call.js // This type represents the form of the callSingleKeyserverEndpoint function // that gets passed to the action functions in lib/actions. export type CallSingleKeyserverEndpoint = ( endpoint: Endpoint, input: Object, options?: ?CallSingleKeyserverEndpointOptions, ) => Promise; type RequestData = { input: { +[key: string]: mixed }, cookie?: ?string, sessionID?: ?string, platformDetails?: PlatformDetails, }; async function callSingleKeyserverEndpoint( cookie: ?string, setNewSession: (sessionChange: ClientSessionChange, error: ?string) => void, waitIfCookieInvalidated: () => Promise, cookieInvalidationRecovery: ( sessionChange: ClientSessionChange, error: ?string, ) => Promise, urlPrefix: string, sessionID: ?string, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, socketAPIHandler: ?SocketAPIHandler, endpoint: Endpoint, input: { +[key: string]: mixed }, dispatch: Dispatch, options?: ?CallSingleKeyserverEndpointOptions, loggedIn: boolean, keyserverID: string, ): Promise { const possibleReplacement = await waitIfCookieInvalidated(); if (possibleReplacement) { return await possibleReplacement(endpoint, input, options); } const shouldSendPlatformDetails = lastCommunicatedPlatformDetails && !_isEqual(lastCommunicatedPlatformDetails)(getConfig().platformDetails); if ( endpointIsSocketPreferred(endpoint) && isSocketConnected && socketAPIHandler && !options?.urlPrefixOverride ) { try { const result = await socketAPIHandler({ endpoint, input }); options?.getResultInfo?.({ interface: 'socket' }); return result; } catch (e) { if (endpointIsSocketOnly(endpoint)) { throw e; } else if (e instanceof SocketOffline) { // nothing } else if (e instanceof SocketTimeout) { // nothing } else { throw e; } } } if (endpointIsSocketOnly(endpoint)) { throw new SocketOffline('socket_offline'); } const resolvedURLPrefix = options?.urlPrefixOverride ?? urlPrefix; const url = resolvedURLPrefix ? `${resolvedURLPrefix}/${endpoint}` : endpoint; let json; if (options && options.performHTTPMultipartUpload) { const performHTTPMultipartUpload = typeof options.performHTTPMultipartUpload === 'function' ? options.performHTTPMultipartUpload : defaultPerformHTTPMultipartUpload; json = await performHTTPMultipartUpload( url, cookie, sessionID, input, options, ); } else { const mergedData: RequestData = { input }; mergedData.cookie = cookie ? cookie : null; if (getConfig().setSessionIDOnRequest) { // We make sure that if setSessionIDOnRequest is true, we never set // sessionID to undefined. null has a special meaning here: we cannot // consider the cookieID to be a unique session identifier, but we do not // have a sessionID to use either. This should only happen when the user // is not logged in on web. mergedData.sessionID = sessionID ? sessionID : null; } if (shouldSendPlatformDetails) { mergedData.platformDetails = getConfig().platformDetails; } const callEndpointPromise = (async (): Promise => { const response = await fetch(url, { method: 'POST', // This is necessary to allow cookie headers to get passed down to us credentials: 'same-origin', body: JSON.stringify(mergedData), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); const text = await response.text(); try { return JSON.parse(text); } catch (e) { console.log(text); throw e; } })(); const timeout = options && options.timeout ? options.timeout : callSingleKeyserverEndpointTimeout; if (!timeout) { json = await callEndpointPromise; } else { const rejectPromise = (async () => { await sleep(timeout); throw new FetchTimeout( `callSingleKeyserverEndpoint timed out call to ${endpoint}`, endpoint, ); })(); json = await Promise.race([callEndpointPromise, rejectPromise]); } } - extractAndPersistUserInfosFromEndpointResponse(json, endpoint); + extractAndPersistUserInfosFromEndpointResponse(json, endpoint, dispatch); const { cookieChange, error, payload, currentUserInfo } = json; const sessionChange: ?ServerSessionChange = cookieChange; if (sessionChange) { const { threadInfos, userInfos, ...rest } = sessionChange; const clientSessionChange = rest.cookieInvalidated ? rest : { cookieInvalidated: false, currentUserInfo, ...rest }; if (clientSessionChange.cookieInvalidated) { const maybeReplacement = await cookieInvalidationRecovery( clientSessionChange, error, ); if (maybeReplacement) { return await maybeReplacement(endpoint, input, options); } } else { // We don't want to call setNewSession when cookieInvalidated. If the // cookie is invalidated, the cookieInvalidationRecovery call above will // either trigger a invalidation recovery attempt (if supported), or it // will call setNewSession itself. If the invalidation recovery is // attempted, it will result in a setNewSession call when it concludes. setNewSession(clientSessionChange, error); } } if (!error && shouldSendPlatformDetails) { dispatch({ type: updateLastCommunicatedPlatformDetailsActionType, payload: { platformDetails: getConfig().platformDetails, keyserverID }, }); } if (error === 'policies_not_accepted' && loggedIn) { dispatch({ type: forcePolicyAcknowledgmentActionType, payload, }); } if (error) { throw new ServerError(error, payload); } options?.getResultInfo?.({ interface: 'REST' }); return json; } export default callSingleKeyserverEndpoint; diff --git a/lib/socket/user-infos-handler.react.js b/lib/socket/user-infos-handler.react.js index 7f09a3a55..24b398931 100644 --- a/lib/socket/user-infos-handler.react.js +++ b/lib/socket/user-infos-handler.react.js @@ -1,39 +1,49 @@ // @flow import * as React from 'react'; +import { processNewUserIDsActionType } from '../actions/user-actions.js'; import { serverServerSocketMessageValidator, type SocketListener, type ClientServerSocketMessage, } from '../types/socket-types.js'; import { extractUserIDsFromPayload } from '../utils/conversion-utils.js'; +import { useDispatch } from '../utils/redux-utils.js'; type Props = { +addListener: (listener: SocketListener) => mixed, +removeListener: (listener: SocketListener) => mixed, }; export default function SocketMessageUserInfosHandler( props: Props, ): React.Node { - const onMessage = React.useCallback((message: ClientServerSocketMessage) => { - // eslint-disable-next-line no-unused-vars - const newUserIDs = extractUserIDsFromPayload( - serverServerSocketMessageValidator, - message, - ); - // TODO: dispatch an action adding the new user ids to the UserStore - }, []); + const dispatch = useDispatch(); + const onMessage = React.useCallback( + (message: ClientServerSocketMessage) => { + const newUserIDs = extractUserIDsFromPayload( + serverServerSocketMessageValidator, + message, + ); + if (newUserIDs.length > 0) { + dispatch({ + type: processNewUserIDsActionType, + payload: { userIDs: newUserIDs }, + }); + } + }, + [dispatch], + ); const { addListener, removeListener } = props; React.useEffect(() => { addListener(onMessage); return () => { removeListener(onMessage); }; }, [addListener, removeListener, onMessage]); return null; } diff --git a/lib/utils/user-info-extraction-utils.js b/lib/utils/user-info-extraction-utils.js index 288e74a16..565b2a0ff 100644 --- a/lib/utils/user-info-extraction-utils.js +++ b/lib/utils/user-info-extraction-utils.js @@ -1,85 +1,92 @@ // @flow import _memoize from 'lodash/memoize.js'; import t, { type TType, type TInterface } from 'tcomb'; +import { processNewUserIDsActionType } from '../actions/user-actions.js'; import type { CallSingleKeyserverEndpointResponse } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { mixedRawThreadInfoValidator } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; import type { Endpoint } from '../types/endpoints.js'; +import type { Dispatch } from '../types/redux-types.js'; import type { MixedRawThreadInfos } from '../types/thread-types.js'; import { userInfoValidator } from '../types/user-types.js'; import type { UserInfo } from '../types/user-types.js'; import { endpointValidators } from '../types/validators/endpoint-validators.js'; import { extractUserIDsFromPayload } from '../utils/conversion-utils.js'; import { tID, tShape } from '../utils/validation-utils.js'; type AdditionalCookieChange = { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, +cookieInvalidated?: boolean, sessionID?: string, cookie?: string, }; type AdditionalResponseFields = { +cookieChange: AdditionalCookieChange, +error?: string, +payload?: Object, +success: boolean, }; const additionalResponseFieldsValidator = tShape({ cookieChange: t.maybe( tShape({ threadInfos: t.dict(tID, mixedRawThreadInfoValidator), userInfos: t.list(userInfoValidator), cookieInvalidated: t.maybe(t.Boolean), sessionID: t.maybe(t.String), cookie: t.maybe(t.String), }), ), error: t.maybe(t.String), payload: t.maybe(t.Object), success: t.maybe(t.Boolean), }); function extendResponderValidatorBase(inputValidator: TType): TType { if (inputValidator.meta.kind === 'union') { const newTypes = []; for (const innerValidator of inputValidator.meta.types) { const newInnerValidator = extendResponderValidatorBase(innerValidator); newTypes.push(newInnerValidator); } return t.union(newTypes); } else if (inputValidator.meta.kind === 'interface') { const recastValidator: TInterface = (inputValidator: any); return (tShape({ ...recastValidator.meta.props, ...additionalResponseFieldsValidator.meta.props, }): any); } else if (inputValidator.meta.kind === 'maybe') { const typeObj = extendResponderValidatorBase(inputValidator.meta.type); return (t.maybe(typeObj): any); } else if (inputValidator.meta.kind === 'subtype') { return extendResponderValidatorBase(inputValidator.meta.type); } return inputValidator; } const extendResponderValidator = _memoize(extendResponderValidatorBase); function extractAndPersistUserInfosFromEndpointResponse( message: CallSingleKeyserverEndpointResponse, endpoint: Endpoint, + dispatch: Dispatch, ): void { const extendedValidator = extendResponderValidator( endpointValidators[endpoint].validator, ); - // eslint-disable-next-line no-unused-vars const newUserIDs = extractUserIDsFromPayload(extendedValidator, message); - // TODO: dispatch an action adding the new user ids to the UserStore + if (newUserIDs.length > 0) { + dispatch({ + type: processNewUserIDsActionType, + payload: { userIDs: newUserIDs }, + }); + } } export { extractAndPersistUserInfosFromEndpointResponse };