diff --git a/lib/actions/activity-actions.js b/lib/actions/activity-actions.js index 9d91ebde2..48bc4646d 100644 --- a/lib/actions/activity-actions.js +++ b/lib/actions/activity-actions.js @@ -1,103 +1,103 @@ // @flow import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { ActivityUpdate, ActivityUpdateSuccessPayload, SetThreadUnreadStatusPayload, SetThreadUnreadStatusRequest, } from '../types/activity-types.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; export type UpdateActivityInput = { +activityUpdates: $ReadOnlyArray, }; const updateActivityActionTypes = Object.freeze({ started: 'UPDATE_ACTIVITY_STARTED', success: 'UPDATE_ACTIVITY_SUCCESS', failed: 'UPDATE_ACTIVITY_FAILED', }); const updateActivity = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: UpdateActivityInput) => Promise) => async input => { const { activityUpdates } = input; const requests: { [string]: { +updates: ActivityUpdate[] } } = {}; for (const update of activityUpdates) { const keyserverID = extractKeyserverIDFromID(update.threadID); if (!requests[keyserverID]) { requests[keyserverID] = { updates: [] }; } requests[keyserverID].updates.push(update); } const responses = await callKeyserverEndpoint('update_activity', requests); let unfocusedToUnread: $ReadOnlyArray = []; for (const keyserverID in responses) { unfocusedToUnread = unfocusedToUnread.concat( responses[keyserverID].unfocusedToUnread, ); } const sortedActivityUpdates: { [keyserverID: string]: $ReadOnlyArray, } = {}; for (const keyserverID in requests) { sortedActivityUpdates[keyserverID] = requests[keyserverID].updates; } return { activityUpdates: sortedActivityUpdates, result: { unfocusedToUnread, }, }; }; function useUpdateActivity(): ( input: UpdateActivityInput, ) => Promise { return useKeyserverCall(updateActivity); } const setThreadUnreadStatusActionTypes = Object.freeze({ started: 'SET_THREAD_UNREAD_STATUS_STARTED', success: 'SET_THREAD_UNREAD_STATUS_SUCCESS', failed: 'SET_THREAD_UNREAD_STATUS_FAILED', }); const setThreadUnreadStatus = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SetThreadUnreadStatusRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'set_thread_unread_status', requests, ); return { resetToUnread: responses[keyserverID].resetToUnread, threadID: input.threadID, }; }; function useSetThreadUnreadStatus(): ( request: SetThreadUnreadStatusRequest, ) => Promise { return useKeyserverCall(setThreadUnreadStatus); } export { updateActivityActionTypes, useUpdateActivity, setThreadUnreadStatusActionTypes, useSetThreadUnreadStatus, }; diff --git a/lib/actions/device-actions.js b/lib/actions/device-actions.js index e7da3f016..d1e09d224 100644 --- a/lib/actions/device-actions.js +++ b/lib/actions/device-actions.js @@ -1,126 +1,124 @@ // @flow +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { GetVersionActionPayload, DeviceTokenUpdateRequest, } from '../types/device-types'; import { getConfig } from '../utils/config.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import type { - CallKeyserverEndpoint, - KeyserverCallParamOverride, -} from '../utils/keyserver-call.js'; +import type { KeyserverCallParamOverride } from '../utils/keyserver-call.js'; export type DeviceTokens = { +[keyserverID: string]: ?string, }; export type SetDeviceTokenActionPayload = { +deviceTokens: DeviceTokens, }; const setDeviceTokenActionTypes = Object.freeze({ started: 'SET_DEVICE_TOKEN_STARTED', success: 'SET_DEVICE_TOKEN_SUCCESS', failed: 'SET_DEVICE_TOKEN_FAILED', }); const setDeviceToken = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeviceTokens) => Promise) => async input => { const requests: { [string]: DeviceTokenUpdateRequest } = {}; for (const keyserverID in input) { requests[keyserverID] = { deviceToken: input[keyserverID], platformDetails: getConfig().platformDetails, }; } await callKeyserverEndpoint('update_device_token', requests); return { deviceTokens: input }; }; function useSetDeviceToken(): ( input: DeviceTokens, ) => Promise { return useKeyserverCall(setDeviceToken); } const setDeviceTokenFanout = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: ?string) => Promise) => async input => { const requests: { [string]: DeviceTokenUpdateRequest } = {}; const deviceTokens: { [string]: ?string } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = { deviceToken: input, platformDetails: getConfig().platformDetails, }; deviceTokens[keyserverID] = input; } await callKeyserverEndpoint('update_device_token', requests); return { deviceTokens }; }; function useSetDeviceTokenFanout(): ( input: ?string, ) => Promise { return useKeyserverCall(setDeviceTokenFanout); } const getVersionActionTypes = Object.freeze({ started: 'GET_VERSION_STARTED', success: 'GET_VERSION_SUCCESS', failed: 'GET_VERSION_FAILED', }); // useGetVersion should be passed paramOverride containing keyserverInfos // with entries for the keyservers we want to connect to. // They should contain the urlPrefix of the keyserver. // The key should also be the urlPrefix, // since the id of the keyserver is not yet known. const getVersion = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } const responses = await callKeyserverEndpoint('version', requests); const result = { versionResponses: {} }; for (const keyserverID in responses) { const { codeVersion, ownerUsername, ownerID } = responses[keyserverID]; result.versionResponses[ownerID] = { codeVersion, ownerUsername, ownerID, }; } return result; }; function useGetVersion( paramOverride?: ?KeyserverCallParamOverride, ): () => Promise { return useKeyserverCall(getVersion, paramOverride); } const updateLastCommunicatedPlatformDetailsActionType = 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS'; export { setDeviceTokenActionTypes, useSetDeviceToken, useSetDeviceTokenFanout, getVersionActionTypes, useGetVersion, updateLastCommunicatedPlatformDetailsActionType, }; diff --git a/lib/actions/entry-actions.js b/lib/actions/entry-actions.js index d3eb0ca7f..731914b83 100644 --- a/lib/actions/entry-actions.js +++ b/lib/actions/entry-actions.js @@ -1,344 +1,344 @@ // @flow import { extractKeyserverIDFromID, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { localIDPrefix } from '../shared/message-utils.js'; import type { RawEntryInfo, CalendarQuery, SaveEntryInfo, SaveEntryResult, CreateEntryInfo, CreateEntryPayload, DeleteEntryInfo, DeleteEntryResult, RestoreEntryInfo, RestoreEntryResult, FetchEntryInfosResult, CalendarQueryUpdateResult, } from '../types/entry-types.js'; import type { HistoryRevisionInfo } from '../types/history-types.js'; import { dateFromString } from '../utils/date-utils.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const fetchEntriesActionTypes = Object.freeze({ started: 'FETCH_ENTRIES_STARTED', success: 'FETCH_ENTRIES_SUCCESS', failed: 'FETCH_ENTRIES_FAILED', }); const fetchEntries = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((calendarQuery: CalendarQuery) => Promise) => async calendarQuery => { const calendarQueries = sortCalendarQueryPerKeyserver( calendarQuery, allKeyserverIDs, ); const requests: { [string]: CalendarQuery } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = calendarQueries[keyserverID]; } const responses = await callKeyserverEndpoint('fetch_entries', requests); let rawEntryInfos: $ReadOnlyArray = []; for (const keyserverID in responses) { rawEntryInfos = rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } return { rawEntryInfos, }; }; function useFetchEntries(): ( calendarQuery: CalendarQuery, ) => Promise { return useKeyserverCall(fetchEntries); } export type UpdateCalendarQueryInput = { +calendarQuery: CalendarQuery, +reduxAlreadyUpdated?: boolean, }; const updateCalendarQueryActionTypes = Object.freeze({ started: 'UPDATE_CALENDAR_QUERY_STARTED', success: 'UPDATE_CALENDAR_QUERY_SUCCESS', failed: 'UPDATE_CALENDAR_QUERY_FAILED', }); const updateCalendarQuery = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (( input: UpdateCalendarQueryInput, ) => Promise) => async input => { const { calendarQuery, reduxAlreadyUpdated = false } = input; const calendarQueries = sortCalendarQueryPerKeyserver( calendarQuery, allKeyserverIDs, ); const requests: { [string]: CalendarQuery } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = calendarQueries[keyserverID]; } const responses = await callKeyserverEndpoint( 'update_calendar_query', requests, ); let rawEntryInfos: $ReadOnlyArray = []; let deletedEntryIDs: $ReadOnlyArray = []; for (const keyserverID in responses) { rawEntryInfos = rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); deletedEntryIDs = deletedEntryIDs.concat( responses[keyserverID].deletedEntryIDs, ); } return { rawEntryInfos, deletedEntryIDs, calendarQuery, calendarQueryAlreadyUpdated: reduxAlreadyUpdated, }; }; function useUpdateCalendarQuery(): ( input: UpdateCalendarQueryInput, ) => Promise { return useKeyserverCall(updateCalendarQuery); } const createLocalEntryActionType = 'CREATE_LOCAL_ENTRY'; function createLocalEntry( threadID: string, localID: number, dateString: string, creatorID: string, ): RawEntryInfo { const date = dateFromString(dateString); const newEntryInfo: RawEntryInfo = { localID: `${localIDPrefix}${localID}`, threadID, text: '', year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime: Date.now(), creatorID, deleted: false, }; return newEntryInfo; } const createEntryActionTypes = Object.freeze({ started: 'CREATE_ENTRY_STARTED', success: 'CREATE_ENTRY_SUCCESS', failed: 'CREATE_ENTRY_FAILED', }); const createEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], }, }; const response = await callKeyserverEndpoint('create_entry', requests); return { entryID: response[keyserverID].entryID, newMessageInfos: response[keyserverID].newMessageInfos, threadID: response[keyserverID].threadID, localID: response[keyserverID].localID, updatesResult: response[keyserverID].updatesResult, }; }; function useCreateEntry(): ( request: CreateEntryInfo, ) => Promise { return useKeyserverCall(createEntry); } const saveEntryActionTypes = Object.freeze({ started: 'SAVE_ENTRY_STARTED', success: 'SAVE_ENTRY_SUCCESS', failed: 'SAVE_ENTRY_FAILED', }); const concurrentModificationResetActionType = 'CONCURRENT_MODIFICATION_RESET'; const saveEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SaveEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], }, }; const response = await callKeyserverEndpoint('update_entry', requests); return { entryID: response[keyserverID].entryID, newMessageInfos: response[keyserverID].newMessageInfos, updatesResult: response[keyserverID].updatesResult, }; }; function useSaveEntry(): (input: SaveEntryInfo) => Promise { return useKeyserverCall(saveEntry); } const deleteEntryActionTypes = Object.freeze({ started: 'DELETE_ENTRY_STARTED', success: 'DELETE_ENTRY_SUCCESS', failed: 'DELETE_ENTRY_FAILED', }); const deleteEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((info: DeleteEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], timestamp: Date.now(), }, }; const response = await callKeyserverEndpoint('delete_entry', requests); return { newMessageInfos: response[keyserverID].newMessageInfos, threadID: response[keyserverID].threadID, updatesResult: response[keyserverID].updatesResult, }; }; function useDeleteEntry(): ( info: DeleteEntryInfo, ) => Promise { return useKeyserverCall(deleteEntry); } export type FetchRevisionsForEntryInput = { +entryID: string, }; const fetchRevisionsForEntryActionTypes = Object.freeze({ started: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', success: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', failed: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', }); const fetchRevisionsForEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchRevisionsForEntryInput, ) => Promise<$ReadOnlyArray>) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const requests = { [keyserverID]: { id: input.entryID, }, }; const response = await callKeyserverEndpoint( 'fetch_entry_revisions', requests, ); return response[keyserverID].result; }; function useFetchRevisionsForEntry(): ( input: FetchRevisionsForEntryInput, ) => Promise<$ReadOnlyArray> { return useKeyserverCall(fetchRevisionsForEntry); } const restoreEntryActionTypes = Object.freeze({ started: 'RESTORE_ENTRY_STARTED', success: 'RESTORE_ENTRY_SUCCESS', failed: 'RESTORE_ENTRY_FAILED', }); const restoreEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RestoreEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], timestamp: Date.now(), }, }; const response = await callKeyserverEndpoint('restore_entry', requests); return { newMessageInfos: response[keyserverID].newMessageInfos, updatesResult: response[keyserverID].updatesResult, }; }; function useRestoreEntry(): ( input: RestoreEntryInfo, ) => Promise { return useKeyserverCall(restoreEntry); } export { fetchEntriesActionTypes, useFetchEntries, updateCalendarQueryActionTypes, useUpdateCalendarQuery, createLocalEntryActionType, createLocalEntry, createEntryActionTypes, useCreateEntry, saveEntryActionTypes, concurrentModificationResetActionType, useSaveEntry, deleteEntryActionTypes, useDeleteEntry, fetchRevisionsForEntryActionTypes, useFetchRevisionsForEntry, restoreEntryActionTypes, useRestoreEntry, }; diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js index 91d07ef99..764ee6c7a 100644 --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -1,150 +1,150 @@ // @flow import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { FetchInviteLinksResponse, InviteLinkVerificationRequest, InviteLinkVerificationResponse, CreateOrUpdatePublicLinkRequest, InviteLink, DisableInviteLinkRequest, DisableInviteLinkPayload, } from '../types/link-types.js'; import type { CallSingleKeyserverEndpoint } from '../utils/call-single-keyserver-endpoint.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const verifyInviteLinkActionTypes = Object.freeze({ started: 'VERIFY_INVITE_LINK_STARTED', success: 'VERIFY_INVITE_LINK_SUCCESS', failed: 'VERIFY_INVITE_LINK_FAILED', }); const verifyInviteLink = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( request: InviteLinkVerificationRequest, ) => Promise) => async request => { const response = await callSingleKeyserverEndpoint( 'verify_invite_link', request, ); if (response.status === 'valid' || response.status === 'already_joined') { return { status: response.status, community: response.community, }; } return { status: response.status, }; }; const fetchPrimaryInviteLinkActionTypes = Object.freeze({ started: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', success: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', failed: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', }); const fetchPrimaryInviteLinks = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { const requests: { [string]: void } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = undefined; } const responses = await callKeyserverEndpoint( 'fetch_primary_invite_links', requests, ); let links: $ReadOnlyArray = []; for (const keyserverID in responses) { links = links.concat(responses[keyserverID].links); } return { links, }; }; function useFetchPrimaryInviteLinks(): () => Promise { return useKeyserverCall(fetchPrimaryInviteLinks); } const createOrUpdatePublicLinkActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', success: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', failed: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', }); const createOrUpdatePublicLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateOrUpdatePublicLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: { name: input.name, communityID: input.communityID, }, }; const responses = await callKeyserverEndpoint( 'create_or_update_public_link', requests, ); const response = responses[keyserverID]; return { name: response.name, primary: response.primary, role: response.role, communityID: response.communityID, expirationTime: response.expirationTime, limitOfUses: response.limitOfUses, numberOfUses: response.numberOfUses, }; }; function useCreateOrUpdatePublicLink(): ( input: CreateOrUpdatePublicLinkRequest, ) => Promise { return useKeyserverCall(createOrUpdatePublicLink); } const disableInviteLinkLinkActionTypes = Object.freeze({ started: 'DISABLE_INVITE_LINK_STARTED', success: 'DISABLE_INVITE_LINK_SUCCESS', failed: 'DISABLE_INVITE_LINK_FAILED', }); const disableInviteLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DisableInviteLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: input }; await callKeyserverEndpoint('disable_invite_link', requests); return input; }; function useDisableInviteLink(): ( input: DisableInviteLinkRequest, ) => Promise { return useKeyserverCall(disableInviteLink); } export { verifyInviteLinkActionTypes, verifyInviteLink, fetchPrimaryInviteLinkActionTypes, useFetchPrimaryInviteLinks, createOrUpdatePublicLinkActionTypes, useCreateOrUpdatePublicLink, disableInviteLinkLinkActionTypes, useDisableInviteLink, }; diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js index 2e4195982..c216a39ae 100644 --- a/lib/actions/message-actions.js +++ b/lib/actions/message-actions.js @@ -1,530 +1,530 @@ // @flow import invariant from 'invariant'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { FetchMessageInfosPayload, SendMessageResult, SendEditMessageResult, SendReactionMessageRequest, SimpleMessagesPayload, SendEditMessageRequest, FetchPinnedMessagesRequest, FetchPinnedMessagesResult, SearchMessagesRequest, SearchMessagesResponse, FetchMessageInfosRequest, RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { MediaMessageServerDBContent } from '../types/messages/media.js'; import type { ToggleMessagePinRequest, ToggleMessagePinResult, } from '../types/thread-types.js'; import type { CallSingleKeyserverEndpointResultInfo } from '../utils/call-single-keyserver-endpoint.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const fetchMessagesBeforeCursorActionTypes = Object.freeze({ started: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', success: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', failed: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', }); export type FetchMessagesBeforeCursorInput = { +threadID: string, +beforeMessageID: string, }; const fetchMessagesBeforeCursor = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchMessagesBeforeCursorInput, ) => Promise) => async input => { const { threadID, beforeMessageID } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { cursors: { [threadID]: beforeMessageID, }, }, }; const responses = await callKeyserverEndpoint('fetch_messages', requests); return { threadID, rawMessageInfos: responses[keyserverID].rawMessageInfos, truncationStatus: responses[keyserverID].truncationStatuses[threadID], }; }; function useFetchMessagesBeforeCursor(): ( input: FetchMessagesBeforeCursorInput, ) => Promise { return useKeyserverCall(fetchMessagesBeforeCursor); } export type FetchMostRecentMessagesInput = { +threadID: string, }; const fetchMostRecentMessagesActionTypes = Object.freeze({ started: 'FETCH_MOST_RECENT_MESSAGES_STARTED', success: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', failed: 'FETCH_MOST_RECENT_MESSAGES_FAILED', }); const fetchMostRecentMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchMostRecentMessagesInput, ) => Promise) => async input => { const { threadID } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { cursors: { [threadID]: null, }, }, }; const responses = await callKeyserverEndpoint('fetch_messages', requests); return { threadID, rawMessageInfos: responses[keyserverID].rawMessageInfos, truncationStatus: responses[keyserverID].truncationStatuses[threadID], }; }; function useFetchMostRecentMessages(): ( input: FetchMostRecentMessagesInput, ) => Promise { return useKeyserverCall(fetchMostRecentMessages); } const fetchSingleMostRecentMessagesFromThreadsActionTypes = Object.freeze({ started: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', success: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', failed: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', }); const fetchSingleMostRecentMessagesFromThreads = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((threadIDs: $ReadOnlyArray) => Promise) => async threadIDs => { const sortedThreadIDs = sortThreadIDsPerKeyserver(threadIDs); const requests: { [string]: FetchMessageInfosRequest } = {}; for (const keyserverID in sortedThreadIDs) { const cursors = Object.fromEntries( sortedThreadIDs[keyserverID].map(threadID => [threadID, null]), ); requests[keyserverID] = { cursors, numberPerThread: 1, }; } const responses = await callKeyserverEndpoint('fetch_messages', requests); let rawMessageInfos: $ReadOnlyArray = []; let truncationStatuses: MessageTruncationStatuses = {}; for (const keyserverID in responses) { rawMessageInfos = rawMessageInfos.concat( responses[keyserverID].rawMessageInfos, ); truncationStatuses = { ...truncationStatuses, ...responses[keyserverID].truncationStatuses, }; } return { rawMessageInfos, truncationStatuses, }; }; function useFetchSingleMostRecentMessagesFromThreads(): ( threadIDs: $ReadOnlyArray, ) => Promise { return useKeyserverCall(fetchSingleMostRecentMessagesFromThreads); } export type SendTextMessageInput = { +threadID: string, +localID: string, +text: string, +sidebarCreation?: boolean, }; const sendTextMessageActionTypes = Object.freeze({ started: 'SEND_TEXT_MESSAGE_STARTED', success: 'SEND_TEXT_MESSAGE_SUCCESS', failed: 'SEND_TEXT_MESSAGE_FAILED', }); const sendTextMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendTextMessageInput) => Promise) => async input => { let resultInfo; const getResultInfo = ( passedResultInfo: CallSingleKeyserverEndpointResultInfo, ) => { resultInfo = passedResultInfo; }; const { threadID, localID, text, sidebarCreation } = input; let payload = { threadID, localID, text }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_text_message', requests, { getResultInfo, }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendTextMessage(): ( input: SendTextMessageInput, ) => Promise { return useKeyserverCall(sendTextMessage); } const createLocalMessageActionType = 'CREATE_LOCAL_MESSAGE'; export type SendMultimediaMessageInput = { +threadID: string, +localID: string, +mediaMessageContents: $ReadOnlyArray, +sidebarCreation?: boolean, }; const sendMultimediaMessageActionTypes = Object.freeze({ started: 'SEND_MULTIMEDIA_MESSAGE_STARTED', success: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', failed: 'SEND_MULTIMEDIA_MESSAGE_FAILED', }); const sendMultimediaMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendMultimediaMessageInput) => Promise) => async input => { let resultInfo; const getResultInfo = ( passedResultInfo: CallSingleKeyserverEndpointResultInfo, ) => { resultInfo = passedResultInfo; }; const { threadID, localID, mediaMessageContents, sidebarCreation } = input; let payload = { threadID, localID, mediaMessageContents }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_multimedia_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendMultimediaMessage(): ( input: SendMultimediaMessageInput, ) => Promise { return useKeyserverCall(sendMultimediaMessage); } export type LegacySendMultimediaMessageInput = { +threadID: string, +localID: string, +mediaIDs: $ReadOnlyArray, +sidebarCreation?: boolean, }; const legacySendMultimediaMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: LegacySendMultimediaMessageInput, ) => Promise) => async input => { let resultInfo; const getResultInfo = ( passedResultInfo: CallSingleKeyserverEndpointResultInfo, ) => { resultInfo = passedResultInfo; }; const { threadID, localID, mediaIDs, sidebarCreation } = input; let payload = { threadID, localID, mediaIDs }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_multimedia_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useLegacySendMultimediaMessage(): ( input: LegacySendMultimediaMessageInput, ) => Promise { return useKeyserverCall(legacySendMultimediaMessage); } const sendReactionMessageActionTypes = Object.freeze({ started: 'SEND_REACTION_MESSAGE_STARTED', success: 'SEND_REACTION_MESSAGE_SUCCESS', failed: 'SEND_REACTION_MESSAGE_FAILED', }); const sendReactionMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendReactionMessageRequest) => Promise) => async input => { let resultInfo; const getResultInfo = ( passedResultInfo: CallSingleKeyserverEndpointResultInfo, ) => { resultInfo = passedResultInfo; }; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { threadID: input.threadID, localID: input.localID, targetMessageID: input.targetMessageID, reaction: input.reaction, action: input.action, }, }; const responses = await callKeyserverEndpoint( 'create_reaction_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendReactionMessage(): ( input: SendReactionMessageRequest, ) => Promise { return useKeyserverCall(sendReactionMessage); } const sendEditMessageActionTypes = Object.freeze({ started: 'SEND_EDIT_MESSAGE_STARTED', success: 'SEND_EDIT_MESSAGE_SUCCESS', failed: 'SEND_EDIT_MESSAGE_FAILED', }); const sendEditMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendEditMessageRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.targetMessageID); const requests = { [keyserverID]: { targetMessageID: input.targetMessageID, text: input.text, }, }; const responses = await callKeyserverEndpoint('edit_message', requests); return { newMessageInfos: responses[keyserverID].newMessageInfos, }; }; function useSendEditMessage(): ( input: SendEditMessageRequest, ) => Promise { return useKeyserverCall(sendEditMessage); } const saveMessagesActionType = 'SAVE_MESSAGES'; const processMessagesActionType = 'PROCESS_MESSAGES'; const messageStorePruneActionType = 'MESSAGE_STORE_PRUNE'; const fetchPinnedMessageActionTypes = Object.freeze({ started: 'FETCH_PINNED_MESSAGES_STARTED', success: 'FETCH_PINNED_MESSAGES_SUCCESS', failed: 'FETCH_PINNED_MESSAGES_FAILED', }); const fetchPinnedMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchPinnedMessagesRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'fetch_pinned_messages', requests, ); return { pinnedMessages: responses[keyserverID].pinnedMessages }; }; function useFetchPinnedMessages(): ( input: FetchPinnedMessagesRequest, ) => Promise { return useKeyserverCall(fetchPinnedMessages); } const searchMessagesActionTypes = Object.freeze({ started: 'SEARCH_MESSAGES_STARTED', success: 'SEARCH_MESSAGES_SUCCESS', failed: 'SEARCH_MESSAGES_FAILED', }); const searchMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SearchMessagesRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('search_messages', requests); return { messages: responses[keyserverID].messages, endReached: responses[keyserverID].endReached, }; }; function useSearchMessages(): ( input: SearchMessagesRequest, ) => Promise { return useKeyserverCall(searchMessages); } const toggleMessagePinActionTypes = Object.freeze({ started: 'TOGGLE_MESSAGE_PIN_STARTED', success: 'TOGGLE_MESSAGE_PIN_SUCCESS', failed: 'TOGGLE_MESSAGE_PIN_FAILED', }); const toggleMessagePin = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ToggleMessagePinRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.messageID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'toggle_message_pin', requests, ); const response = responses[keyserverID]; return { newMessageInfos: response.newMessageInfos, threadID: response.threadID, }; }; function useToggleMessagePin(): ( input: ToggleMessagePinRequest, ) => Promise { return useKeyserverCall(toggleMessagePin); } export { fetchMessagesBeforeCursorActionTypes, useFetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, useFetchMostRecentMessages, fetchSingleMostRecentMessagesFromThreadsActionTypes, useFetchSingleMostRecentMessagesFromThreads, sendTextMessageActionTypes, useSendTextMessage, createLocalMessageActionType, sendMultimediaMessageActionTypes, useSendMultimediaMessage, useLegacySendMultimediaMessage, searchMessagesActionTypes, useSearchMessages, sendReactionMessageActionTypes, useSendReactionMessage, saveMessagesActionType, processMessagesActionType, messageStorePruneActionType, sendEditMessageActionTypes, useSendEditMessage, useFetchPinnedMessages, fetchPinnedMessageActionTypes, toggleMessagePinActionTypes, useToggleMessagePin, }; diff --git a/lib/actions/message-report-actions.js b/lib/actions/message-report-actions.js index 39a00ebf0..bf0c85d8f 100644 --- a/lib/actions/message-report-actions.js +++ b/lib/actions/message-report-actions.js @@ -1,41 +1,41 @@ // @flow import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { MessageReportCreationRequest, MessageReportCreationResult, } from '../types/message-report-types.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; const sendMessageReportActionTypes = Object.freeze({ started: 'SEND_MESSAGE_REPORT_STARTED', success: 'SEND_MESSAGE_REPORT_SUCCESS', failed: 'SEND_MESSAGE_REPORT_FAILED', }); const sendMessageReport = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: MessageReportCreationRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.messageID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'create_message_report', requests, ); const response = responses[keyserverID]; return { messageInfo: response.messageInfo }; }; function useSendMessageReport(): ( input: MessageReportCreationRequest, ) => Promise { return useKeyserverCall(sendMessageReport); } export { sendMessageReportActionTypes, useSendMessageReport }; diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js index b6a0cc71d..b4c6c543d 100644 --- a/lib/actions/thread-actions.js +++ b/lib/actions/thread-actions.js @@ -1,357 +1,357 @@ // @flow import invariant from 'invariant'; import genesis from '../facts/genesis.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { ChangeThreadSettingsPayload, LeaveThreadPayload, UpdateThreadRequest, ClientNewThreadRequest, NewThreadResult, ClientThreadJoinRequest, ThreadJoinPayload, ThreadFetchMediaRequest, ThreadFetchMediaResult, RoleModificationRequest, RoleModificationPayload, RoleDeletionRequest, RoleDeletionPayload, } from '../types/thread-types.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import { values } from '../utils/objects.js'; export type DeleteThreadInput = { +threadID: string, }; const deleteThreadActionTypes = Object.freeze({ started: 'DELETE_THREAD_STARTED', success: 'DELETE_THREAD_SUCCESS', failed: 'DELETE_THREAD_FAILED', }); const deleteThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeleteThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('delete_thread', requests); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useDeleteThread(): ( input: DeleteThreadInput, ) => Promise { return useKeyserverCall(deleteThread); } const changeThreadSettingsActionTypes = Object.freeze({ started: 'CHANGE_THREAD_SETTINGS_STARTED', success: 'CHANGE_THREAD_SETTINGS_SUCCESS', failed: 'CHANGE_THREAD_SETTINGS_FAILED', }); const changeThreadSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: UpdateThreadRequest) => Promise) => async input => { invariant( Object.keys(input.changes).length > 0, 'No changes provided to changeThreadSettings!', ); const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('update_thread', requests); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useChangeThreadSettings(): ( input: UpdateThreadRequest, ) => Promise { return useKeyserverCall(changeThreadSettings); } export type RemoveUsersFromThreadInput = { +threadID: string, +memberIDs: $ReadOnlyArray, }; const removeUsersFromThreadActionTypes = Object.freeze({ started: 'REMOVE_USERS_FROM_THREAD_STARTED', success: 'REMOVE_USERS_FROM_THREAD_SUCCESS', failed: 'REMOVE_USERS_FROM_THREAD_FAILED', }); const removeUsersFromThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: RemoveUsersFromThreadInput, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('remove_members', requests); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useRemoveUsersFromThread(): ( input: RemoveUsersFromThreadInput, ) => Promise { return useKeyserverCall(removeUsersFromThread); } export type ChangeThreadMemberRolesInput = { +threadID: string, +memberIDs: $ReadOnlyArray, +newRole: string, }; const changeThreadMemberRolesActionTypes = Object.freeze({ started: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', success: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', failed: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', }); const changeThreadMemberRoles = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: ChangeThreadMemberRolesInput, ) => Promise) => async input => { const { threadID, memberIDs, newRole } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { threadID, memberIDs, role: newRole, }, }; const responses = await callKeyserverEndpoint('update_role', requests); const response = responses[keyserverID]; return { threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useChangeThreadMemberRoles(): ( input: ChangeThreadMemberRolesInput, ) => Promise { return useKeyserverCall(changeThreadMemberRoles); } const newThreadActionTypes = Object.freeze({ started: 'NEW_THREAD_STARTED', success: 'NEW_THREAD_SUCCESS', failed: 'NEW_THREAD_FAILED', }); const newThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientNewThreadRequest) => Promise) => async input => { const parentThreadID = input.parentThreadID ?? genesis.id; const keyserverID = extractKeyserverIDFromID(parentThreadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('create_thread', requests); const response = responses[keyserverID]; return { newThreadID: response.newThreadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, userInfos: response.userInfos, }; }; function useNewThread(): ( input: ClientNewThreadRequest, ) => Promise { return useKeyserverCall(newThread); } const joinThreadActionTypes = Object.freeze({ started: 'JOIN_THREAD_STARTED', success: 'JOIN_THREAD_SUCCESS', failed: 'JOIN_THREAD_FAILED', }); const joinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientThreadJoinRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('join_thread', requests); const response = responses[keyserverID]; const userInfos = values(response.userInfos); return { updatesResult: response.updatesResult, rawMessageInfos: response.rawMessageInfos, truncationStatuses: response.truncationStatuses, userInfos, }; }; function useJoinThread(): ( input: ClientThreadJoinRequest, ) => Promise { return useKeyserverCall(joinThread); } export type LeaveThreadInput = { +threadID: string, }; const leaveThreadActionTypes = Object.freeze({ started: 'LEAVE_THREAD_STARTED', success: 'LEAVE_THREAD_SUCCESS', failed: 'LEAVE_THREAD_FAILED', }); const leaveThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LeaveThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('leave_thread', requests); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useLeaveThread(): ( input: LeaveThreadInput, ) => Promise { return useKeyserverCall(leaveThread); } const fetchThreadMedia = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ThreadFetchMediaRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'fetch_thread_media', requests, ); const response = responses[keyserverID]; return { media: response.media }; }; function useFetchThreadMedia(): ( input: ThreadFetchMediaRequest, ) => Promise { return useKeyserverCall(fetchThreadMedia); } const modifyCommunityRoleActionTypes = Object.freeze({ started: 'MODIFY_COMMUNITY_ROLE_STARTED', success: 'MODIFY_COMMUNITY_ROLE_SUCCESS', failed: 'MODIFY_COMMUNITY_ROLE_FAILED', }); const modifyCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleModificationRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'modify_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useModifyCommunityRole(): ( input: RoleModificationRequest, ) => Promise { return useKeyserverCall(modifyCommunityRole); } const deleteCommunityRoleActionTypes = Object.freeze({ started: 'DELETE_COMMUNITY_ROLE_STARTED', success: 'DELETE_COMMUNITY_ROLE_SUCCESS', failed: 'DELETE_COMMUNITY_ROLE_FAILED', }); const deleteCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleDeletionRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'delete_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useDeleteCommunityRole(): ( input: RoleDeletionRequest, ) => Promise { return useKeyserverCall(deleteCommunityRole); } export { deleteThreadActionTypes, useDeleteThread, changeThreadSettingsActionTypes, useChangeThreadSettings, removeUsersFromThreadActionTypes, useRemoveUsersFromThread, changeThreadMemberRolesActionTypes, useChangeThreadMemberRoles, newThreadActionTypes, useNewThread, joinThreadActionTypes, useJoinThread, leaveThreadActionTypes, useLeaveThread, useFetchThreadMedia, modifyCommunityRoleActionTypes, useModifyCommunityRole, deleteCommunityRoleActionTypes, useDeleteCommunityRole, }; diff --git a/lib/actions/upload-actions.js b/lib/actions/upload-actions.js index f86b9dd26..762d76143 100644 --- a/lib/actions/upload-actions.js +++ b/lib/actions/upload-actions.js @@ -1,251 +1,251 @@ // @flow import uuid from 'uuid'; import blobService from '../facts/blob-service.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import type { UploadMultimediaResult, Dimensions } from '../types/media-types'; import { toBase64URL } from '../utils/base64.js'; import { blobServiceUploadHandler, type BlobServiceUploadHandler, } from '../utils/blob-service-upload.js'; import { makeBlobServiceEndpointURL } from '../utils/blob-service.js'; import type { CallSingleKeyserverEndpoint } from '../utils/call-single-keyserver-endpoint.js'; import { getMessageForException } from '../utils/errors.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; import { handleHTTPResponseError } from '../utils/services-utils.js'; import { type UploadBlob } from '../utils/upload-blob.js'; export type MultimediaUploadCallbacks = Partial<{ +onProgress: (percent: number) => void, +abortHandler: (abort: () => void) => void, +uploadBlob: UploadBlob, +blobServiceUploadHandler: BlobServiceUploadHandler, +timeout: ?number, }>; export type MultimediaUploadExtras = $ReadOnly< Partial<{ ...Dimensions, +loop: boolean, +encryptionKey: string, +thumbHash: ?string, }>, >; const uploadMultimedia = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( multimedia: Object, extras: MultimediaUploadExtras, callbacks?: MultimediaUploadCallbacks, ) => Promise) => async (multimedia, extras, callbacks) => { const onProgress = callbacks && callbacks.onProgress; const abortHandler = callbacks && callbacks.abortHandler; const uploadBlob = callbacks && callbacks.uploadBlob; const stringExtras: { [string]: string } = {}; if (extras.height !== null && extras.height !== undefined) { stringExtras.height = extras.height.toString(); } if (extras.width !== null && extras.width !== undefined) { stringExtras.width = extras.width.toString(); } if (extras.loop) { stringExtras.loop = '1'; } if (extras.encryptionKey) { stringExtras.encryptionKey = extras.encryptionKey; } if (extras.thumbHash) { stringExtras.thumbHash = extras.thumbHash; } // also pass MIME type if available if (multimedia.type && typeof multimedia.type === 'string') { stringExtras.mimeType = multimedia.type; } const response = await callSingleKeyserverEndpoint( 'upload_multimedia', { ...stringExtras, multimedia: [multimedia], }, { onProgress, abortHandler, blobUpload: uploadBlob ? uploadBlob : true, }, ); const [uploadResult] = response.results; return { id: uploadResult.id, uri: uploadResult.uri, dimensions: uploadResult.dimensions, mediaType: uploadResult.mediaType, loop: uploadResult.loop, }; }; export type DeleteUploadInput = { +id: string, +keyserverOrThreadID: string, }; const updateMultimediaMessageMediaActionType = 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA'; const deleteUpload = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeleteUploadInput) => Promise) => async input => { const { id, keyserverOrThreadID } = input; const keyserverID = extractKeyserverIDFromID(keyserverOrThreadID); const requests = { [keyserverID]: { id } }; await callKeyserverEndpoint('delete_upload', requests); }; function useDeleteUpload(): (input: DeleteUploadInput) => Promise { return useKeyserverCall(deleteUpload); } export type BlobServiceUploadFile = | { +type: 'file', +file: File } | { +type: 'uri', +uri: string, +filename: string, +mimeType: string, }; export type BlobServiceUploadInput = { +blobInput: BlobServiceUploadFile, +blobHash: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash?: ?string, +loop?: boolean, }; export type BlobServiceUploadResult = { ...UploadMultimediaResult, blobHolder: ?string, }; export type BlobServiceUploadAction = (input: { +uploadInput: BlobServiceUploadInput, +keyserverOrThreadID: string, +callbacks?: MultimediaUploadCallbacks, }) => Promise; const blobServiceUpload = (callKeyserverEndpoint: CallKeyserverEndpoint): BlobServiceUploadAction => async input => { const { uploadInput, callbacks, keyserverOrThreadID } = input; const { encryptionKey, loop, dimensions, thumbHash, blobInput } = uploadInput; const blobHolder = uuid.v4(); const blobHash = toBase64URL(uploadInput.blobHash); // 1. Assign new holder for blob with given blobHash let blobAlreadyExists: boolean; try { const assignHolderEndpoint = blobService.httpEndpoints.ASSIGN_HOLDER; const assignHolderResponse = await fetch( makeBlobServiceEndpointURL(assignHolderEndpoint), { method: assignHolderEndpoint.method, body: JSON.stringify({ holder: blobHolder, blob_hash: blobHash, }), headers: { 'content-type': 'application/json', }, }, ); handleHTTPResponseError(assignHolderResponse); const { data_exists: dataExistsResponse } = await assignHolderResponse.json(); blobAlreadyExists = dataExistsResponse; } catch (e) { throw new Error( `Failed to assign holder: ${ getMessageForException(e) ?? 'unknown error' }`, ); } // 2. Upload blob contents if blob doesn't exist if (!blobAlreadyExists) { const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB; let blobServiceUploadCallback = blobServiceUploadHandler; if (callbacks && callbacks.blobServiceUploadHandler) { blobServiceUploadCallback = callbacks.blobServiceUploadHandler; } try { await blobServiceUploadCallback( makeBlobServiceEndpointURL(uploadEndpoint), uploadEndpoint.method, { blobHash, blobInput, }, { ...callbacks }, ); } catch (e) { throw new Error( `Failed to upload blob: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } // 3. Upload metadata to keyserver const keyserverID = extractKeyserverIDFromID(keyserverOrThreadID); const requests = { [keyserverID]: { blobHash, blobHolder, encryptionKey, filename: blobInput.type === 'file' ? blobInput.file.name : blobInput.filename, mimeType: blobInput.type === 'file' ? blobInput.file.type : blobInput.mimeType, loop, thumbHash, ...dimensions, }, }; const responses = await callKeyserverEndpoint( 'upload_media_metadata', requests, ); const response = responses[keyserverID]; return { id: response.id, uri: response.uri, mediaType: response.mediaType, dimensions: response.dimensions, loop: response.loop, blobHolder, }; }; function useBlobServiceUpload(): BlobServiceUploadAction { return useKeyserverCall(blobServiceUpload); } export { uploadMultimedia, useBlobServiceUpload, updateMultimediaMessageMediaActionType, useDeleteUpload, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index e1e70fc15..425ce368f 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,752 +1,752 @@ // @flow import * as React from 'react'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { preRequestUserStateSelector } from '../selectors/account-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, KeyserverAuthRequest, ClientLogInResponse, KeyserverLogOutResult, } 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 { IdentityRegisterResult } from '../types/identity-service-types.js'; 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 { MinimallyEncodedRawThreadInfos } from '../types/thread-types'; import type { CurrentUserInfo, UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../utils/call-single-keyserver-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, }; export type KeyserverLogOutInput = { +preRequestUserState: PreRequestUserState, +keyserverIDs?: $ReadOnlyArray, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { 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, keyserverIDs }; }; function useLogOut(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverLogOut = useKeyserverCall(logOut); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverLogOut({ preRequestUserState, keyserverIDs }), [callKeyserverLogOut, preRequestUserState], ); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallSingleKeyserverEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [ashoatKeyserverID]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallSingleKeyserverEndpointOptions, }); 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: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState, keyserverIDs, }; }; function useDeleteKeyserverAccount(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = useSelector(preRequestUserStateSelector); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverDeleteAccount({ preRequestUserState, keyserverIDs }), [callKeyserverDeleteAccount, preRequestUserState], ); } const deleteIdentityAccountActionTypes = Object.freeze({ started: 'DELETE_IDENTITY_ACCOUNT_STARTED', success: 'DELETE_IDENTITY_ACCOUNT_SUCCESS', failed: 'DELETE_IDENTITY_ACCOUNT_FAILED', }); function useDeleteIdentityAccount(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; return React.useCallback(() => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return identityClient.deleteUser(); }, [identityClient]); } const keyserverRegisterActionTypes = Object.freeze({ started: 'KEYSERVER_REGISTER_STARTED', success: 'KEYSERVER_REGISTER_SUCCESS', failed: 'KEYSERVER_REGISTER_FAILED', }); const registerCallSingleKeyserverEndpointOptions = { timeout: 60000 }; const keyserverRegister = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( registerInfo: RegisterInfo, options?: CallSingleKeyserverEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[ashoatKeyserverID]; const response = await callSingleKeyserverEndpoint( 'create_account', { ...registerInfo, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...registerCallSingleKeyserverEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; export type KeyserverAuthInput = $ReadOnly<{ ...KeyserverAuthInfo, +preRequestUserInfo: ?CurrentUserInfo, }>; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallSingleKeyserverEndpointOptions = { timeout: 60000 }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInput) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, preRequestUserInfo, ...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]: ClientLogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: MinimallyEncodedRawThreadInfos = {}; 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, preRequestUserInfo, }; }; function useKeyserverAuth(): ( input: KeyserverAuthInfo, ) => Promise { const preRequestUserInfo = useSelector(state => state.currentUserInfo); const callKeyserverAuth = useKeyserverCall(keyserverAuth); return React.useCallback( (input: KeyserverAuthInfo) => callKeyserverAuth({ preRequestUserInfo, ...input }), [callKeyserverAuth, preRequestUserInfo], ); } const identityRegisterActionTypes = Object.freeze({ started: 'IDENTITY_REGISTER_STARTED', success: 'IDENTITY_REGISTER_SUCCESS', failed: 'IDENTITY_REGISTER_FAILED', }); function useIdentityRegister(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; return React.useCallback( (username, password) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } if (!identityClient.registerUser) { throw new Error('Register user method unimplemented'); } return identityClient.registerUser(username, password); }, [identityClient], ); } 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 tempIdentityLoginActionTypes = Object.freeze({ started: 'TEMP_IDENTITY_LOG_IN_STARTED', success: 'TEMP_IDENTITY_LOG_IN_SUCCESS', failed: 'TEMP_IDENTITY_LOG_IN_FAILED', }); const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallSingleKeyserverEndpointOptions = { 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]: ClientLogInResponse } = await callKeyserverEndpoint( 'log_in', requests, logInCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: MinimallyEncodedRawThreadInfos = {}; 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 changeKeyserverUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', success: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', failed: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', }); const changeKeyserverUserPassword = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callSingleKeyserverEndpoint('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 = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((username: string) => Promise) => async username => { const response = await callSingleKeyserverEndpoint('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 = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callSingleKeyserverEndpoint('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 = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async options => { return await callSingleKeyserverEndpoint( 'get_olm_session_initialization_data', {}, options, ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callSingleKeyserverEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callSingleKeyserverEndpoint('update_user_avatar', avatarDBContent); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn as logInRawAction, tempIdentityLoginActionTypes, useLogIn, logInActionTypes, useLogOut, logOutActionTypes, keyserverRegister, keyserverRegisterActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, deleteIdentityAccountActionTypes, useDeleteIdentityAccount, keyserverAuthActionTypes, useKeyserverAuth, identityRegisterActionTypes, useIdentityRegister, }; diff --git a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js index 261a10ad3..66abdecb7 100644 --- a/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js +++ b/lib/keyserver-conn/call-keyserver-endpoint-provider.react.js @@ -1,319 +1,321 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { createSelector } from 'reselect'; import { useKeyserverCallInfos } from './keyserver-call-infos.js'; import { setNewSession } from './keyserver-conn-types.js'; import { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, } from './recovery-utils.js'; import { logInActionSources } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../utils/call-single-keyserver-endpoint.js'; import callSingleKeyserverEndpoint from '../utils/call-single-keyserver-endpoint.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; type CreateCallSingleKeyserverEndpointSelector = ( keyserverID: string, ) => ServerCallSelectorParams => CallSingleKeyserverEndpoint; + type GetCallSingleKeyserverEndpoint = ( keyserverID: string, ) => CallSingleKeyserverEndpoint; + type CallKeyserverEndpointContextType = { +createCallSingleKeyserverEndpointSelector: CreateCallSingleKeyserverEndpointSelector, +getCallSingleKeyserverEndpoint: GetCallSingleKeyserverEndpoint, }; const CallKeyserverEndpointContext: React.Context = React.createContext(); type OngoingRecoveryAttempt = { +waitingCalls: Array< (callSingleKeyserverEndpoint: ?CallSingleKeyserverEndpoint) => mixed, >, }; export type ServerCallSelectorParams = { +dispatch: Dispatch, +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +isSocketConnected: boolean, +lastCommunicatedPlatformDetails: ?PlatformDetails, }; type BindServerCallsParams = $ReadOnly<{ ...ServerCallSelectorParams, +keyserverID: string, }>; type Props = { +children: React.Node, }; function CallKeyserverEndpointProvider(props: Props): React.Node { // SECTION 1: bindCookieAndUtilsIntoCallServerEndpoint const ongoingRecoveryAttemptsRef = React.useRef< Map, >(new Map()); const bindCookieAndUtilsIntoCallSingleKeyserverEndpoint: ( params: BindServerCallsParams, ) => CallSingleKeyserverEndpoint = React.useCallback(params => { const { dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, } = params; const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); const boundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => setNewSession( dispatch, sessionChange, { currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }, error, undefined, keyserverID, ); const canResolveInvalidation = canResolveKeyserverSessionInvalidation(); // This function gets called before callSingleKeyserverEndpoint sends a // request, to make sure that we're not in the middle of trying to recover // an invalidated cookie const waitIfCookieInvalidated = () => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callSingleKeyserverEndpoint instance continue return Promise.resolve(null); } const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); if (!ongoingRecoveryAttempt) { // Our cookie seems to be valid return Promise.resolve(null); } // Wait to run until we get our new cookie return new Promise(r => ongoingRecoveryAttempt.waitingCalls.push(r), ); }; // This function is a helper for the next function defined below const attemptToResolveInvalidation = async ( sessionChange: ClientSessionChange, ) => { const newAnonymousCookie = sessionChange.cookie; const newSessionChange = await resolveKeyserverSessionInvalidation( dispatch, newAnonymousCookie, urlPrefix, logInActionSources.cookieInvalidationResolutionAttempt, keyserverID, ); const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); ongoingRecoveryAttemptsRef.current.delete(keyserverID); const currentWaitingCalls = ongoingRecoveryAttempt?.waitingCalls ?? []; const newCallSingleKeyserverEndpoint = newSessionChange ? bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ ...params, cookie: newSessionChange.cookie, sessionID: newSessionChange.sessionID, currentUserInfo: newSessionChange.currentUserInfo, }) : null; for (const func of currentWaitingCalls) { func(newCallSingleKeyserverEndpoint); } return newCallSingleKeyserverEndpoint; }; // If this function is called, callSingleKeyserverEndpoint got a response // invalidating its cookie, and is wondering if it should just like... // give up? Or if there's a chance at redemption const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { if (!canResolveInvalidation) { // If there is no way to resolve the session invalidation, // just let the caller callSingleKeyserverEndpoint instance continue return Promise.resolve(null); } if (!loggedIn) { // We don't want to attempt any use native credentials of a logged out // user to log-in after a cookieInvalidation while logged out return Promise.resolve(null); } const ongoingRecoveryAttempt = ongoingRecoveryAttemptsRef.current.get(keyserverID); if (ongoingRecoveryAttempt) { return new Promise(r => ongoingRecoveryAttempt.waitingCalls.push(r), ); } ongoingRecoveryAttemptsRef.current.set(keyserverID, { waitingCalls: [] }); return attemptToResolveInvalidation(sessionChange); }; return ( endpoint: Endpoint, data: Object, options?: ?CallSingleKeyserverEndpointOptions, ) => callSingleKeyserverEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, socketAPIHandler, endpoint, data, dispatch, options, loggedIn, keyserverID, ); }, []); // SECTION 2: createCallSingleKeyserverEndpointSelector // For each keyserver, we have a set of params that configure our connection // to it. These params get bound into callSingleKeyserverEndpoint before it's // passed to an ActionFunc. This helper function lets us create a selector for // a given keyserverID that will regenerate the bound // callSingleKeyserverEndpoint function only if one of the params changes. // This lets us skip some React render cycles. const createCallSingleKeyserverEndpointSelector = React.useCallback( ( keyserverID: string, ): (ServerCallSelectorParams => CallSingleKeyserverEndpoint) => createSelector( (params: ServerCallSelectorParams) => params.dispatch, (params: ServerCallSelectorParams) => params.cookie, (params: ServerCallSelectorParams) => params.urlPrefix, (params: ServerCallSelectorParams) => params.sessionID, (params: ServerCallSelectorParams) => params.currentUserInfo, (params: ServerCallSelectorParams) => params.isSocketConnected, (params: ServerCallSelectorParams) => params.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, isSocketConnected: boolean, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallSingleKeyserverEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, isSocketConnected, lastCommunicatedPlatformDetails, keyserverID, }), ), [bindCookieAndUtilsIntoCallSingleKeyserverEndpoint], ); // SECTION 3: getCallSingleKeyserverEndpoint const dispatch = useDispatch(); const currentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const keyserverCallInfos = useKeyserverCallInfos(keyserverInfos); const callSingleKeyserverEndpointSelectorCacheRef = React.useRef< Map CallSingleKeyserverEndpoint>, >(new Map()); const getCallSingleKeyserverEndpoint: GetCallSingleKeyserverEndpoint = React.useCallback( (keyserverID: string) => { let selector = callSingleKeyserverEndpointSelectorCacheRef.current.get(keyserverID); if (!selector) { selector = createCallSingleKeyserverEndpointSelector(keyserverID); callSingleKeyserverEndpointSelectorCacheRef.current.set( keyserverID, selector, ); } const keyserverCallInfo = keyserverCallInfos[keyserverID]; return selector({ ...keyserverCallInfo, dispatch, currentUserInfo, }); }, [ createCallSingleKeyserverEndpointSelector, dispatch, currentUserInfo, keyserverCallInfos, ], ); const value = React.useMemo( () => ({ createCallSingleKeyserverEndpointSelector, getCallSingleKeyserverEndpoint, }), [createCallSingleKeyserverEndpointSelector, getCallSingleKeyserverEndpoint], ); return ( {props.children} ); } function useCallKeyserverEndpointContext(): CallKeyserverEndpointContextType { const callKeyserverEndpointContext = React.useContext( CallKeyserverEndpointContext, ); invariant( callKeyserverEndpointContext, 'callKeyserverEndpointContext should be set', ); return callKeyserverEndpointContext; } let socketAPIHandler: ?SocketAPIHandler = null; function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { socketAPIHandler = passedSocketAPIHandler; } export { CallKeyserverEndpointProvider, useCallKeyserverEndpointContext, registerActiveSocket, }; diff --git a/lib/keyserver-conn/keyserver-conn-types.js b/lib/keyserver-conn/keyserver-conn-types.js index 358680821..7079a6254 100644 --- a/lib/keyserver-conn/keyserver-conn-types.js +++ b/lib/keyserver-conn/keyserver-conn-types.js @@ -1,51 +1,73 @@ // @flow import type { LogInActionSource, LogInStartingPayload, LogInResult, } from '../types/account-types.js'; +import type { Endpoint } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ClientSessionChange, PreRequestUserState, } from '../types/session-types.js'; +import type { + CallSingleKeyserverEndpoint, + CallSingleKeyserverEndpointOptions, +} from '../utils/call-single-keyserver-endpoint.js'; export type ActionTypes< STARTED_ACTION_TYPE: string, SUCCESS_ACTION_TYPE: string, FAILED_ACTION_TYPE: string, > = { started: STARTED_ACTION_TYPE, success: SUCCESS_ACTION_TYPE, failed: FAILED_ACTION_TYPE, }; export type DispatchRecoveryAttempt = ( actionTypes: ActionTypes<'LOG_IN_STARTED', 'LOG_IN_SUCCESS', 'LOG_IN_FAILED'>, promise: Promise, startingPayload: LogInStartingPayload, ) => Promise; const setNewSessionActionType = 'SET_NEW_SESSION'; function setNewSession( dispatch: Dispatch, sessionChange: ClientSessionChange, preRequestUserState: ?PreRequestUserState, error: ?string, logInActionSource: ?LogInActionSource, keyserverID: string, ) { dispatch({ type: setNewSessionActionType, payload: { sessionChange, preRequestUserState, error, logInActionSource, keyserverID, }, }); } +export type SingleKeyserverActionFunc = ( + callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, +) => F; + +export type CallKeyserverEndpoint = ( + endpoint: Endpoint, + requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, + options?: ?CallSingleKeyserverEndpointOptions, +) => Promise<{ +[keyserverID: string]: any }>; +export type ActionFunc = ( + callSingleKeyserverEndpoint: CallKeyserverEndpoint, + // The second argument is only used in actions that call all keyservers, + // and the request to all keyservers are exactly the same. + // An example of such action is fetchEntries. + allKeyserverIDs: $ReadOnlyArray, +) => Args => Promise; + export { setNewSessionActionType, setNewSession }; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js index 3a984a7be..a93ca1b9d 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,54 +1,50 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import type { CallSingleKeyserverEndpoint } from './call-single-keyserver-endpoint.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; import { type ServerCallSelectorParams, useCallKeyserverEndpointContext, } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; +import type { SingleKeyserverActionFunc } from '../keyserver-conn/keyserver-conn-types.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; -type ActionFunc = ( - callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, -) => F; - function useLegacyAshoatKeyserverCall( - serverCall: ActionFunc, + serverCall: SingleKeyserverActionFunc, paramOverride?: ?Partial, ): F { const dispatch = useDispatch(); const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); const { createCallSingleKeyserverEndpointSelector } = useCallKeyserverEndpointContext(); const selector = React.useMemo( () => createCallSingleKeyserverEndpointSelector(ashoatKeyserverID), [createCallSingleKeyserverEndpointSelector], ); return React.useMemo(() => { const { urlPrefix, isSocketConnected } = serverCallState; invariant( !!urlPrefix && isSocketConnected !== undefined && isSocketConnected !== null, 'keyserver missing from keyserverStore', ); const callSingleKeyserverEndpoint = selector({ ...serverCallState, urlPrefix, isSocketConnected, dispatch, ...paramOverride, }); return serverCall(callSingleKeyserverEndpoint); }, [serverCall, serverCallState, dispatch, paramOverride, selector]); } export { useLegacyAshoatKeyserverCall }; diff --git a/lib/utils/config.js b/lib/utils/config.js index b34460924..7770ce36e 100644 --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -1,39 +1,41 @@ // @flow import invariant from 'invariant'; import type { CallSingleKeyserverEndpoint } from './call-single-keyserver-endpoint.js'; -import type { CallKeyserverEndpoint } from './keyserver-call.js'; -import type { DispatchRecoveryAttempt } from '../keyserver-conn/keyserver-conn-types.js'; +import type { + DispatchRecoveryAttempt, + CallKeyserverEndpoint, +} from '../keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from '../shared/crypto-utils.js'; import type { LogInActionSource } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; export type Config = { +resolveKeyserverSessionInvalidationUsingNativeCredentials: ?( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( options?: ?InitialNotifMessageOptions, ) => Promise, ) => Promise, +setSessionIDOnRequest: boolean, +calendarRangeInactivityLimit: ?number, +platformDetails: PlatformDetails, }; let registeredConfig: ?Config = null; const registerConfig = (config: Config) => { registeredConfig = { ...registeredConfig, ...config }; }; const getConfig = (): Config => { invariant(registeredConfig, 'config should be set'); return registeredConfig; }; export { registerConfig, getConfig }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js index 0a860e739..7716c0ebb 100644 --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -1,120 +1,107 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import type { CallSingleKeyserverEndpointOptions } from './call-single-keyserver-endpoint.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { useCallKeyserverEndpointContext } from '../keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { useKeyserverCallInfos, type KeyserverInfoPartial, } from '../keyserver-conn/keyserver-call-infos.js'; +import type { ActionFunc } from '../keyserver-conn/keyserver-conn-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; -export type CallKeyserverEndpoint = ( - endpoint: Endpoint, - requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, - options?: ?CallSingleKeyserverEndpointOptions, -) => Promise<{ +[keyserverID: string]: any }>; - -type ActionFunc = ( - callSingleKeyserverEndpoint: CallKeyserverEndpoint, - // The second argument is only used in actions that call all keyservers, - // and the request to all keyservers are exactly the same. - // An example of such action is fetchEntries. - allKeyserverIDs: $ReadOnlyArray, -) => Args => Promise; - export type KeyserverCallParamOverride = Partial<{ +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, }>; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const baseDispatch = useDispatch(); const baseCurrentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const baseCombinedInfo = { dispatch: baseDispatch, currentUserInfo: baseCurrentUserInfo, keyserverInfos, ...paramOverride, }; const { dispatch, currentUserInfo, keyserverInfos: keyserverInfoPartials, } = baseCombinedInfo; const keyserverCallInfos = useKeyserverCallInfos(keyserverInfoPartials); const { createCallSingleKeyserverEndpointSelector } = useCallKeyserverEndpointContext(); const getCallSingleKeyserverEndpointSelector: typeof createCallSingleKeyserverEndpointSelector = React.useMemo( () => _memoize(createCallSingleKeyserverEndpointSelector), [createCallSingleKeyserverEndpointSelector], ); return React.useMemo(() => { const callKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallSingleKeyserverEndpointOptions, ) => { const makeCallToSingleKeyserver = (keyserverID: string) => { const { cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, } = keyserverCallInfos[keyserverID]; const boundCallSingleKeyserverEndpoint = getCallSingleKeyserverEndpointSelector(keyserverID)({ dispatch, currentUserInfo, cookie, urlPrefix, sessionID, isSocketConnected, lastCommunicatedPlatformDetails, }); return boundCallSingleKeyserverEndpoint( endpoint, requests[keyserverID], options, ); }; const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = makeCallToSingleKeyserver(keyserverID); } return promiseAll(promises); }; const keyserverIDs = Object.keys(keyserverCallInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, [ dispatch, currentUserInfo, keyserverCallInfos, getCallSingleKeyserverEndpointSelector, keyserverCall, ]); } export { useKeyserverCall }; diff --git a/native/account/legacy-recover-keyserver-session.js b/native/account/legacy-recover-keyserver-session.js index bb409a604..a0873d375 100644 --- a/native/account/legacy-recover-keyserver-session.js +++ b/native/account/legacy-recover-keyserver-session.js @@ -1,51 +1,53 @@ // @flow import { logInActionTypes, logInRawAction } from 'lib/actions/user-actions.js'; -import type { DispatchRecoveryAttempt } from 'lib/keyserver-conn/keyserver-conn-types.js'; +import type { + DispatchRecoveryAttempt, + CallKeyserverEndpoint, +} from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from 'lib/shared/crypto-utils.js'; import type { LogInActionSource } from 'lib/types/account-types.js'; import type { CallSingleKeyserverEndpoint } from 'lib/utils/call-single-keyserver-endpoint.js'; -import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; import { store } from '../redux/redux-setup.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; async function resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( ?InitialNotifMessageOptions, ) => Promise, ) { const keychainCredentials = await fetchNativeKeychainCredentials(); if (!keychainCredentials) { return; } let extraInfo = await nativeLogInExtraInfoSelector(store.getState())(); if (getInitialNotificationsEncryptedMessage) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpoint, }); extraInfo = { ...extraInfo, initialNotificationsEncryptedMessage }; } const { calendarQuery } = extraInfo; await dispatchRecoveryAttempt( logInActionTypes, logInRawAction(callKeyserverEndpoint)({ ...keychainCredentials, ...extraInfo, logInActionSource, keyserverIDs: [keyserverID], }), { calendarQuery }, ); } export { resolveKeyserverSessionInvalidationUsingNativeCredentials }; diff --git a/web/redux/action-types.js b/web/redux/action-types.js index 2241e5d57..92bc46a84 100644 --- a/web/redux/action-types.js +++ b/web/redux/action-types.js @@ -1,186 +1,186 @@ // @flow import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; +import type { CallKeyserverEndpoint } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { useKeyserverCall } from 'lib/utils/keyserver-call.js'; -import type { CallKeyserverEndpoint } from 'lib/utils/keyserver-call.js'; import type { URLInfo } from 'lib/utils/url-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import type { ExcludedData, InitialReduxState, InitialReduxStateResponse, InitialKeyserverInfo, InitialReduxStateRequest, } from '../types/redux-types.js'; export const updateNavInfoActionType = 'UPDATE_NAV_INFO'; export const updateWindowDimensionsActionType = 'UPDATE_WINDOW_DIMENSIONS'; export const updateWindowActiveActionType = 'UPDATE_WINDOW_ACTIVE'; export const setInitialReduxState = 'SET_INITIAL_REDUX_STATE'; const getInitialReduxStateCallSingleKeyserverEndpointOptions = { timeout: 300000, }; type GetInitialReduxStateInput = { +urlInfo: URLInfo, +excludedData: ExcludedData, +allUpdatesCurrentAsOf: { +[keyserverID: string]: number, }, }; const getInitialReduxState = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: GetInitialReduxStateInput) => Promise) => async input => { const requests: { [string]: InitialReduxStateRequest } = {}; const { urlInfo, excludedData, allUpdatesCurrentAsOf } = input; const { thread, inviteSecret, ...rest } = urlInfo; const threadKeyserverID = thread ? extractKeyserverIDFromID(thread) : null; for (const keyserverID of allKeyserverIDs) { // As of Nov 2023, the only validation we have for adding a new keyserver // is we check if the keyserver URL is valid. This is not a very // extensive check, and gives the user the feeling of a false sucesses // when they add new keyservers to the keyserver store. ENG-5371 tracks // the task for initialzing a proper connection with the newly added // keyserver, and at that point we can make the validation checks // for adding a new keyserver more extensive. However, for the time being // we need to add this check below so that we aren't trying to make calls // to nonexistant keyservers that are in our keyserver store. if (keyserverID !== ashoatKeyserverID) { continue; } const clientUpdatesCurrentAsOf = allUpdatesCurrentAsOf[keyserverID]; const keyserverExcludedData: ExcludedData = { threadStore: !!excludedData.threadStore && !!clientUpdatesCurrentAsOf, }; if (keyserverID === threadKeyserverID) { requests[keyserverID] = { urlInfo, excludedData: keyserverExcludedData, clientUpdatesCurrentAsOf, }; } else { requests[keyserverID] = { urlInfo: rest, excludedData: keyserverExcludedData, clientUpdatesCurrentAsOf, }; } } const responses: { +[string]: InitialReduxStateResponse } = await callKeyserverEndpoint( 'get_initial_redux_state', requests, getInitialReduxStateCallSingleKeyserverEndpointOptions, ); const { currentUserInfo, userInfos, pushApiPublicKey, commServicesAccessToken, navInfo, } = responses[ashoatKeyserverID]; const dataLoaded = currentUserInfo && !currentUserInfo.anonymous; const actualizedCalendarQuery = { startDate: navInfo.startDate, endDate: navInfo.endDate, filters: defaultCalendarFilters, }; const entryStore = { daysToEntries: {}, entryInfos: {}, lastUserInteractionCalendar: 0, }; const threadStore = { threadInfos: {}, }; const messageStore = { currentAsOf: {}, local: {}, messages: {}, threads: {}, }; const inviteLinksStore = { links: {}, }; let keyserverInfos: { [keyserverID: string]: InitialKeyserverInfo } = {}; for (const keyserverID in responses) { entryStore.daysToEntries = { ...entryStore.daysToEntries, ...responses[keyserverID].entryStore.daysToEntries, }; entryStore.entryInfos = { ...entryStore.entryInfos, ...responses[keyserverID].entryStore.entryInfos, }; entryStore.lastUserInteractionCalendar = Math.max( entryStore.lastUserInteractionCalendar, responses[keyserverID].entryStore.lastUserInteractionCalendar, ); threadStore.threadInfos = { ...threadStore.threadInfos, ...responses[keyserverID].threadStore.threadInfos, }; messageStore.currentAsOf = { ...messageStore.currentAsOf, ...responses[keyserverID].messageStore.currentAsOf, }; messageStore.messages = { ...messageStore.messages, ...responses[keyserverID].messageStore.messages, }; messageStore.threads = { ...messageStore.threads, ...responses[keyserverID].messageStore.threads, }; inviteLinksStore.links = { ...inviteLinksStore.links, ...responses[keyserverID].inviteLinksStore.links, }; keyserverInfos = { ...keyserverInfos, [keyserverID]: responses[keyserverID].keyserverInfo, }; } return { navInfo: { ...navInfo, inviteSecret, }, currentUserInfo, entryStore, threadStore, userInfos, actualizedCalendarQuery, messageStore, dataLoaded, pushApiPublicKey, commServicesAccessToken, inviteLinksStore, keyserverInfos, }; }; function useGetInitialReduxState(): ( input: GetInitialReduxStateInput, ) => Promise { return useKeyserverCall(getInitialReduxState); } export { useGetInitialReduxState };