diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -4,80 +4,21 @@ import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; -import _isEqual from 'lodash/fp/isEqual.js'; -import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; -import t from 'tcomb'; import { promisify } from 'util'; import { inviteLinkURL } from 'lib/facts/links.js'; -import { baseLegalPolicies } from 'lib/facts/policies.js'; import stores from 'lib/facts/stores.js'; -import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; -import { freshMessageStore } from 'lib/reducers/message-reducer.js'; -import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; -import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; -import { - threadHasPermission, - threadIsPending, - parsePendingThreadID, - createPendingThread, -} from 'lib/shared/thread-utils.js'; -import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; -import { entryStoreValidator } from 'lib/types/entry-types.js'; -import { defaultCalendarFilters } from 'lib/types/filter-types.js'; -import { keyserverStoreValidator } from 'lib/types/keyserver-types.js'; -import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; -import { - defaultNumberPerThread, - messageStoreValidator, -} from 'lib/types/message-types.js'; -import { defaultEnabledReports } from 'lib/types/report-types.js'; -import { defaultConnectionInfo } from 'lib/types/socket-types.js'; -import { threadPermissions } from 'lib/types/thread-permission-types.js'; -import { threadTypes } from 'lib/types/thread-types-enum.js'; -import { threadStoreValidator } from 'lib/types/thread-types.js'; -import { - currentUserInfoValidator, - userInfosValidator, -} from 'lib/types/user-types.js'; -import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; -import { ServerError } from 'lib/utils/errors.js'; -import { promiseAll } from 'lib/utils/promises.js'; -import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; -import { infoFromURL, urlInfoValidator } from 'lib/utils/url-utils.js'; -import { - tBool, - tNumber, - tShape, - tString, - ashoatKeyserverID, -} from 'lib/utils/validation-utils.js'; import getTitle from 'web/title/getTitle.js'; -import { navInfoValidator } from 'web/types/nav-types.js'; -import { navInfoFromURL } from 'web/url-utils.js'; - -import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; -import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; -import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; -import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; -import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; -import { - fetchCurrentUserInfo, - fetchKnownUserInfos, - fetchUserInfos, -} from '../fetchers/user-fetchers.js'; -import { getWebPushConfig } from '../push/providers.js'; -import { setNewSession } from '../session/cookies.js'; + import { Viewer } from '../session/viewer.js'; -import { streamJSON, waitForStream } from '../utils/json-stream.js'; +import { waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL, getCommAppURLFacts, } from '../utils/urls.js'; -import { validateOutput, validateInput } from '../utils/validation-utils.js'; const { renderToNodeStream } = ReactDOMServer; @@ -170,347 +111,17 @@ } } -const initialReduxStateValidator = tShape({ - navInfo: navInfoValidator, - deviceID: t.Nil, - currentUserInfo: currentUserInfoValidator, - draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })), - entryStore: entryStoreValidator, - threadStore: threadStoreValidator, - userStore: tShape({ - userInfos: userInfosValidator, - inconsistencyReports: t.irreducible( - 'default inconsistencyReports', - _isEqual([]), - ), - }), - messageStore: messageStoreValidator, - loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})), - calendarFilters: t.irreducible( - 'defaultCalendarFilters', - _isEqual(defaultCalendarFilters), - ), - communityPickerStore: t.irreducible( - 'default communityPickerStore', - _isEqual({ chat: null, calendar: null }), - ), - windowDimensions: t.irreducible( - 'default windowDimensions', - _isEqual({ width: 0, height: 0 }), - ), - notifPermissionAlertInfo: t.irreducible( - 'default notifPermissionAlertInfo', - _isEqual(defaultNotifPermissionAlertInfo), - ), - actualizedCalendarQuery: tShape({ - startDate: t.String, - endDate: t.String, - filters: t.irreducible('default filters', _isEqual(defaultCalendarFilters)), - }), - watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])), - lifecycleState: tString('active'), - enabledApps: t.irreducible( - 'defaultWebEnabledApps', - _isEqual(defaultWebEnabledApps), - ), - reportStore: t.irreducible( - 'default reportStore', - _isEqual({ - enabledReports: defaultEnabledReports, - queuedReports: [], - }), - ), - nextLocalID: tNumber(0), - deviceToken: t.Nil, - dataLoaded: t.Boolean, - windowActive: tBool(true), - userPolicies: t.irreducible('default userPolicies', _isEqual({})), - cryptoStore: t.irreducible( - 'default cryptoStore', - _isEqual({ - primaryIdentityKeys: null, - notificationIdentityKeys: null, - }), - ), - pushApiPublicKey: t.maybe(t.String), - _persist: t.Nil, - commServicesAccessToken: t.Nil, - inviteLinksStore: inviteLinksStoreValidator, - keyserverStore: keyserverStoreValidator, - initialStateLoaded: tBool(false), -}); - async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise<void> { - const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); - const { basePath, baseDomain } = appURLFacts; + const { basePath } = getAppURLFactsFromRequestURL(req.originalUrl); const baseURL = basePath.replace(/\/$/, ''); - const urlPrefix = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); - const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( - viewer.id, - baseLegalPolicies, - ); - - const initialNavInfoPromise = (async () => { - try { - let urlInfo = infoFromURL(decodeURI(req.url)); - - try { - urlInfo = await validateInput(viewer, urlInfoValidator, urlInfo, true); - } catch (exc) { - // We should still be able to handle older links - if (exc.message !== 'invalid_client_id_prefix') { - throw exc; - } - } - - let backupInfo = { - now: currentDateInTimeZone(viewer.timeZone), - }; - // Some user ids in selectedUserList might not exist in the userStore - // (e.g. they were included in the results of the user search endpoint) - // Because of that we keep their userInfos inside the navInfo. - if (urlInfo.selectedUserList) { - const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); - const userInfos = {}; - for (const userID in fetchedUserInfos) { - const userInfo = fetchedUserInfos[userID]; - if (userInfo.username) { - userInfos[userID] = userInfo; - } - } - backupInfo = { userInfos, ...backupInfo }; - } - return navInfoFromURL(urlInfo, backupInfo); - } catch (e) { - throw new ServerError(e.message); - } - })(); - - const calendarQueryPromise = (async () => { - const initialNavInfo = await initialNavInfoPromise; - return { - startDate: initialNavInfo.startDate, - endDate: initialNavInfo.endDate, - filters: defaultCalendarFilters, - }; - })(); - const messageSelectionCriteria = { joinedThreads: true }; - const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); - const threadInfoPromise = fetchThreadInfos(viewer); - const messageInfoPromise = fetchMessageInfos( - viewer, - messageSelectionCriteria, - defaultNumberPerThread, - ); - const entryInfoPromise = (async () => { - const calendarQuery = await calendarQueryPromise; - return await fetchEntryInfos(viewer, [calendarQuery]); - })(); - const currentUserInfoPromise = fetchCurrentUserInfo(viewer); - const userInfoPromise = fetchKnownUserInfos(viewer); - - const sessionIDPromise = (async () => { - const calendarQuery = await calendarQueryPromise; - if (viewer.loggedIn) { - await setNewSession(viewer, calendarQuery, initialTime); - } - return viewer.sessionID; - })(); - - const threadStorePromise = (async () => { - const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ - threadInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; - })(); - const messageStorePromise = (async () => { - const [ - { threadInfos }, - { rawMessageInfos, truncationStatuses }, - hasNotAcknowledgedPolicies, - ] = await Promise.all([ - threadInfoPromise, - messageInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - if (hasNotAcknowledgedPolicies) { - return { - messages: {}, - threads: {}, - local: {}, - currentAsOf: { [ashoatKeyserverID]: 0 }, - }; - } - const { messageStore: freshStore } = freshMessageStore( - rawMessageInfos, - truncationStatuses, - mostRecentMessageTimestamp(rawMessageInfos, initialTime), - threadInfos, - ); - return freshStore; - })(); - const entryStorePromise = (async () => { - const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ - entryInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - if (hasNotAcknowledgedPolicies) { - return { - entryInfos: {}, - daysToEntries: {}, - lastUserInteractionCalendar: 0, - }; - } - return { - entryInfos: _keyBy('id')(rawEntryInfos), - daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), - lastUserInteractionCalendar: initialTime, - }; - })(); - const userStorePromise = (async () => { - const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ - userInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - return { - userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, - inconsistencyReports: [], - }; - })(); - - const navInfoPromise = (async () => { - const [ - { threadInfos }, - messageStore, - currentUserInfo, - userStore, - finalNavInfo, - ] = await Promise.all([ - threadInfoPromise, - messageStorePromise, - currentUserInfoPromise, - userStorePromise, - initialNavInfoPromise, - ]); - - const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; - if ( - requestedActiveChatThreadID && - !threadIsPending(requestedActiveChatThreadID) && - !threadHasPermission( - threadInfos[requestedActiveChatThreadID], - threadPermissions.VISIBLE, - ) - ) { - finalNavInfo.activeChatThreadID = null; - } - - if (!finalNavInfo.activeChatThreadID) { - const mostRecentThread = mostRecentlyReadThread( - messageStore, - threadInfos, - ); - if (mostRecentThread) { - finalNavInfo.activeChatThreadID = mostRecentThread; - } - } - - if ( - finalNavInfo.activeChatThreadID && - threadIsPending(finalNavInfo.activeChatThreadID) && - finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID - ) { - const pendingThreadData = parsePendingThreadID( - finalNavInfo.activeChatThreadID, - ); - if ( - pendingThreadData && - pendingThreadData.threadType !== threadTypes.SIDEBAR && - currentUserInfo.id - ) { - const { userInfos } = userStore; - const members = [...pendingThreadData.memberIDs, currentUserInfo.id] - .map(id => { - const userInfo = userInfos[id]; - if (!userInfo || !userInfo.username) { - return undefined; - } - const { username } = userInfo; - return { id, username }; - }) - .filter(Boolean); - const newPendingThread = createPendingThread({ - viewerID: currentUserInfo.id, - threadType: pendingThreadData.threadType, - members, - }); - finalNavInfo.activeChatThreadID = newPendingThread.id; - finalNavInfo.pendingThread = newPendingThread; - } - } - - return finalNavInfo; - })(); - const currentAsOfPromise = (async () => { - const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; - return hasNotAcknowledgedPolicies ? 0 : initialTime; - })(); - - const pushApiPublicKeyPromise = (async () => { - const pushConfig = await getWebPushConfig(); - if (!pushConfig) { - if (process.env.NODE_ENV !== 'development') { - console.warn('keyserver/secrets/web_push_config.json should exist'); - } - return null; - } - return pushConfig.publicKey; - })(); - - const inviteLinksStorePromise = (async () => { - const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); - const links = {}; - for (const link of primaryInviteLinks) { - if (link.primary) { - links[link.communityID] = { - primaryLink: link, - }; - } - } - return { - links, - }; - })(); - - const keyserverStorePromise = (async () => { - const { sessionID, updatesCurrentAsOf } = await promiseAll({ - sessionID: sessionIDPromise, - updatesCurrentAsOf: currentAsOfPromise, - }); - - return { - keyserverInfos: { - [ashoatKeyserverID]: { - cookie: undefined, - sessionID, - updatesCurrentAsOf, - urlPrefix, - connection: defaultConnectionInfo, - lastCommunicatedPlatformDetails: null, - }, - }, - }; - })(); - const { jsURL, fontsURL, @@ -562,61 +173,9 @@ reactStream.pipe(res, { end: false }); await waitForStream(reactStream); - res.write(html` + res.end(html` </div> <script> - var preloadedState = - `); - - const initialReduxState = await promiseAll({ - navInfo: navInfoPromise, - deviceID: null, - currentUserInfo: currentUserInfoPromise, - draftStore: { drafts: {} }, - entryStore: entryStorePromise, - threadStore: threadStorePromise, - userStore: userStorePromise, - messageStore: messageStorePromise, - loadingStatuses: {}, - calendarFilters: defaultCalendarFilters, - communityPickerStore: { chat: null, calendar: null }, - windowDimensions: { width: 0, height: 0 }, - notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, - actualizedCalendarQuery: calendarQueryPromise, - watchedThreadIDs: [], - lifecycleState: 'active', - enabledApps: defaultWebEnabledApps, - reportStore: { - enabledReports: defaultEnabledReports, - queuedReports: [], - }, - nextLocalID: 0, - deviceToken: null, - dataLoaded: viewer.loggedIn, - windowActive: true, - userPolicies: {}, - cryptoStore: { - primaryIdentityKeys: null, - notificationIdentityKeys: null, - }, - pushApiPublicKey: pushApiPublicKeyPromise, - _persist: null, - commServicesAccessToken: null, - inviteLinksStore: inviteLinksStorePromise, - keyserverStore: keyserverStorePromise, - initialStateLoaded: false, - }); - const validatedInitialReduxState = validateOutput( - viewer.platformDetails, - initialReduxStateValidator, - initialReduxState, - true, - ); - const jsonStream = streamJSON(res, validatedInitialReduxState); - - await waitForStream(jsonStream); - res.end(html` - ; var baseURL = "${baseURL}"; var olmFilename = "${olmFilename}"; var commQueryExecutorFilename = "${commQueryExecutorFilename}"; diff --git a/web/redux/default-state.js b/web/redux/default-state.js new file mode 100644 --- /dev/null +++ b/web/redux/default-state.js @@ -0,0 +1,93 @@ +// @flow + +import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; +import { defaultCalendarFilters } from 'lib/types/filter-types.js'; +import { defaultConnectionInfo } from 'lib/types/socket-types.js'; +import { isDev } from 'lib/utils/dev-utils.js'; +import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; + +import type { AppState } from './redux-setup.js'; + +const defaultWebState: AppState = Object.freeze({ + navInfo: { + activeChatThreadID: null, + startDate: '', + endDate: '', + tab: 'chat', + }, + currentUserInfo: null, + draftStore: { drafts: {} }, + entryStore: { + entryInfos: {}, + daysToEntries: {}, + lastUserInteractionCalendar: 0, + }, + threadStore: { + threadInfos: {}, + }, + userStore: { + userInfos: {}, + inconsistencyReports: [], + }, + messageStore: { + messages: {}, + threads: {}, + local: {}, + currentAsOf: { [ashoatKeyserverID]: 0 }, + }, + windowActive: true, + pushApiPublicKey: null, + cryptoStore: { + primaryAccount: null, + primaryIdentityKeys: null, + notificationAccount: null, + notificationIdentityKeys: null, + }, + deviceID: null, + windowDimensions: { width: window.width, height: window.height }, + loadingStatuses: {}, + calendarFilters: defaultCalendarFilters, + deviceToken: null, + dataLoaded: false, + notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, + watchedThreadIDs: [], + lifecycleState: 'active', + enabledApps: defaultEnabledApps, + reportStore: { + enabledReports: { + crashReports: false, + inconsistencyReports: false, + mediaReports: false, + }, + queuedReports: [], + }, + nextLocalID: 0, + _persist: null, + userPolicies: {}, + commServicesAccessToken: null, + inviteLinksStore: { + links: {}, + }, + actualizedCalendarQuery: { + startDate: '', + endDate: '', + filters: defaultCalendarFilters, + }, + communityPickerStore: { chat: null, calendar: null }, + keyserverStore: { + keyserverInfos: { + [ashoatKeyserverID]: { + updatesCurrentAsOf: 0, + urlPrefix: isDev + ? 'http://localhost:3000/comm' + : 'https://web.comm.app', + connection: { ...defaultConnectionInfo }, + lastCommunicatedPlatformDetails: null, + }, + }, + }, + initialStateLoaded: false, +}); + +export { defaultWebState }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -35,8 +35,6 @@ import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; -declare var preloadedState: AppState; - const migrations = { [1]: async state => { const { @@ -169,9 +167,7 @@ // the transform might change in the future, but we need to treat // this code like migration code (it shouldn't change). if (oldStorage?._persist?.version === 4) { - const { connection, updatesCurrentAsOf, sessionID } = - preloadedState.keyserverStore.keyserverInfos[ashoatKeyserverID]; - + const defaultConnection = defaultConnectionInfo; return { ...oldStorage, keyserverStore: { @@ -180,9 +176,9 @@ ...oldStorage.keyserverStore.keyserverInfos, [ashoatKeyserverID]: { ...oldStorage.keyserverStore.keyserverInfos[ashoatKeyserverID], - connection, - updatesCurrentAsOf, - sessionID, + connection: { ...defaultConnection }, + updatesCurrentAsOf: 0, + sessionID: null, }, }, }, @@ -223,9 +219,8 @@ keyserverInfos[key] = { ...state.keyserverInfos[key], connection: { ...defaultConnection }, - updatesCurrentAsOf: - preloadedState.keyserverStore.keyserverInfos[key].updatesCurrentAsOf, - sessionID: preloadedState.keyserverStore.keyserverInfos[key].sessionID, + updatesCurrentAsOf: 0, + sessionID: null, }; } return { diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -15,6 +15,7 @@ import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; import ErrorBoundary from './error-boundary.react.js'; +import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; @@ -23,12 +24,10 @@ localforage.config(localforageConfig); -declare var preloadedState: AppState; - const persistedReducer = persistReducer(persistConfig, reducer); const store: Store<AppState, Action> = createStore( persistedReducer, - preloadedState, + defaultWebState, composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), ); const persistor = persistStore(store);