diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index d468da6b0..9412d66da 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,271 +1,270 @@ // @flow import threadWatcher from '../shared/thread-watcher'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, AccessRequest, UpdateUserSettingsRequest, } from '../types/account-types'; -import type { GetUserPublicKeysArgs } from '../types/request-types'; +import type { GetSessionPublicKeysArgs } from '../types/request-types'; import type { UserSearchResult } from '../types/search-types'; -import type { PreRequestUserState } from '../types/session-types'; +import type { + SessionPublicKeys, + PreRequestUserState, +} from '../types/session-types'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types'; -import type { - UserPublicKeys, - UserInfo, - PasswordUpdate, -} from '../types/user-types'; +import type { UserInfo, PasswordUpdate } from '../types/user-types'; import { getConfig } from '../utils/config'; import type { FetchJSON } from '../utils/fetch-json'; import sleep from '../utils/sleep'; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( fetchJSON: FetchJSON, ): (( preRequestUserState: PreRequestUserState, ) => Promise) => async preRequestUserState => { let response = null; try { response = await Promise.race([ fetchJSON('log_out', {}), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? response.currentUserInfo : null; return { currentUserInfo, preRequestUserState }; }; const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const deleteAccount = ( fetchJSON: FetchJSON, ): (( password: string, preRequestUserState: PreRequestUserState, ) => Promise) => async (password, preRequestUserState) => { const response = await fetchJSON('delete_account', { password }); return { currentUserInfo: response.currentUserInfo, preRequestUserState }; }; const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerFetchJSONOptions = { timeout: 60000 }; const register = ( fetchJSON: FetchJSON, ): (( registerInfo: RegisterInfo, ) => Promise) => async registerInfo => { const response = await fetchJSON( 'create_account', { ...registerInfo, platformDetails: getConfig().platformDetails, }, registerFetchJSONOptions, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; 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; } const cookieInvalidationResolutionAttempt = 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT'; const appStartCookieLoggedInButInvalidRedux = 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX'; const appStartReduxLoggedInButInvalidCookie = 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE'; const socketAuthErrorResolutionAttempt = 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT'; const sqliteOpFailure = 'SQLITE_OP_FAILURE'; const sqliteLoadFailure = 'SQLITE_LOAD_FAILURE'; const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInFetchJSONOptions = { timeout: 60000 }; const logIn = ( fetchJSON: FetchJSON, ): ((logInInfo: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { source, ...restLogInInfo } = logInInfo; const response = await fetchJSON( 'log_in', { ...restLogInInfo, watchedIDs, platformDetails: getConfig().platformDetails, }, logInFetchJSONOptions, ); const userInfos = mergeUserInfos( response.userInfos, response.cookieChange.userInfos, ); return { threadInfos: response.cookieChange.threadInfos, currentUserInfo: response.currentUserInfo, calendarResult: { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: response.rawEntryInfos, }, messagesResult: { messageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses, watchedIDsAtRequestTime: watchedIDs, currentAsOf: response.serverTime, }, userInfos, updatesCurrentAsOf: response.serverTime, source, }; }; const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( fetchJSON: FetchJSON, ): (( passwordUpdate: PasswordUpdate, ) => Promise) => async passwordUpdate => { await fetchJSON('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( fetchJSON: FetchJSON, ): (( usernamePrefix: string, ) => Promise) => async usernamePrefix => { const response = await fetchJSON('search_users', { prefix: usernamePrefix }); return { userInfos: response.userInfos, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( fetchJSON: FetchJSON, ): (( subscriptionUpdate: SubscriptionUpdateRequest, ) => Promise) => async subscriptionUpdate => { const response = await fetchJSON( 'update_user_subscription', subscriptionUpdate, ); return { threadID: subscriptionUpdate.threadID, subscription: response.threadSubscription, }; }; const requestAccessActionTypes = Object.freeze({ started: 'REQUEST_ACCESS_STARTED', success: 'REQUEST_ACCESS_SUCCESS', failed: 'REQUEST_ACCESS_FAILED', }); const requestAccess = ( fetchJSON: FetchJSON, ): ((accessRequest: AccessRequest) => Promise) => async accessRequest => { await fetchJSON('request_access', accessRequest); }; const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( fetchJSON: FetchJSON, ): (( userSettingsRequest: UpdateUserSettingsRequest, ) => Promise) => async userSettingsRequest => { await fetchJSON('update_user_settings', userSettingsRequest); }; -const getUserPublicKeys = ( +const getSessionPublicKeys = ( fetchJSON: FetchJSON, ): (( - data: GetUserPublicKeysArgs, -) => Promise) => async data => { - return await fetchJSON('get_user_public_keys', data); + data: GetSessionPublicKeysArgs, +) => Promise) => async data => { + return await fetchJSON('get_session_public_keys', data); }; export { appStartCookieLoggedInButInvalidRedux, appStartReduxLoggedInButInvalidCookie, changeUserPasswordActionTypes, changeUserPassword, cookieInvalidationResolutionAttempt, deleteAccount, deleteAccountActionTypes, - getUserPublicKeys, + getSessionPublicKeys, logIn, logInActionTypes, logOut, logOutActionTypes, register, registerActionTypes, requestAccess, requestAccessActionTypes, searchUsers, searchUsersActionTypes, setUserSettings, setUserSettingsActionTypes, socketAuthErrorResolutionAttempt, sqliteLoadFailure, sqliteOpFailure, updateSubscription, updateSubscriptionActionTypes, }; diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js index 52e0b9220..df07e65b8 100644 --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -1,109 +1,109 @@ // @flow export type APIRequest = { endpoint: Endpoint, input: Object, }; export type SocketAPIHandler = (request: APIRequest) => Promise; export type Endpoint = | HTTPOnlyEndpoint | SocketOnlyEndpoint | HTTPPreferredEndpoint | SocketPreferredEndpoint; // Endpoints that can cause session changes should occur over HTTP, since the // socket code does not currently support changing sessions. In the future they // could be made to work for native, but cookie changes on web require HTTP // since websockets aren't able to Set-Cookie. Note that technically any // endpoint can cause a sessionChange, and in that case the server will close // the socket with a specific error code, and the client will proceed via HTTP. const sessionChangingEndpoints = Object.freeze({ LOG_OUT: 'log_out', DELETE_ACCOUNT: 'delete_account', CREATE_ACCOUNT: 'create_account', LOG_IN: 'log_in', UPDATE_PASSWORD: 'update_password', }); type SessionChangingEndpoint = $Values; // We do uploads over HTTP as well. This is because Websockets use TCP, which // guarantees ordering. That means that if we start an upload, any messages we // try to send the server after the upload starts will have to wait until the // upload ends. To avoid blocking other messages we upload using HTTP // multipart/form-data. const uploadEndpoints = Object.freeze({ UPLOAD_MULTIMEDIA: 'upload_multimedia', }); type UploadEndpoint = $Values; type HTTPOnlyEndpoint = SessionChangingEndpoint | UploadEndpoint; const socketOnlyEndpoints = Object.freeze({ UPDATE_ACTIVITY: 'update_activity', UPDATE_CALENDAR_QUERY: 'update_calendar_query', }); type SocketOnlyEndpoint = $Values; const socketPreferredEndpoints = Object.freeze({ CREATE_ENTRY: 'create_entry', CREATE_ERROR_REPORT: 'create_error_report', CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message', CREATE_TEXT_MESSAGE: 'create_text_message', CREATE_THREAD: 'create_thread', DELETE_ENTRY: 'delete_entry', DELETE_THREAD: 'delete_thread', DELETE_UPLOAD: 'delete_upload', FETCH_ENTRIES: 'fetch_entries', FETCH_ENTRY_REVISIONS: 'fetch_entry_revisions', FETCH_ERROR_REPORT_INFOS: 'fetch_error_report_infos', FETCH_MESSAGES: 'fetch_messages', - GET_USER_PUBLIC_KEYS: 'get_user_public_keys', + GET_SESSION_PUBLIC_KEYS: 'get_session_public_keys', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', REQUEST_ACCESS: 'request_access', RESTORE_ENTRY: 'restore_entry', SEARCH_USERS: 'search_users', SEND_PASSWORD_RESET_EMAIL: 'send_password_reset_email', SEND_VERIFICATION_EMAIL: 'send_verification_email', SET_THREAD_UNREAD_STATUS: 'set_thread_unread_status', UPDATE_ACCOUNT: 'update_account', UPDATE_USER_SETTINGS: 'update_user_settings', UPDATE_DEVICE_TOKEN: 'update_device_token', UPDATE_ENTRY: 'update_entry', UPDATE_RELATIONSHIPS: 'update_relationships', UPDATE_ROLE: 'update_role', UPDATE_THREAD: 'update_thread', UPDATE_USER_SUBSCRIPTION: 'update_user_subscription', VERIFY_CODE: 'verify_code', }); type SocketPreferredEndpoint = $Values; const httpPreferredEndpoints = Object.freeze({ CREATE_REPORT: 'create_report', CREATE_REPORTS: 'create_reports', }); type HTTPPreferredEndpoint = $Values; const socketPreferredEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ]); export function endpointIsSocketPreferred(endpoint: Endpoint): boolean { return socketPreferredEndpointSet.has(endpoint); } const socketSafeEndpointSet = new Set([ ...Object.values(socketOnlyEndpoints), ...Object.values(socketPreferredEndpoints), ...Object.values(httpPreferredEndpoints), ]); export function endpointIsSocketSafe(endpoint: Endpoint): boolean { return socketSafeEndpointSet.has(endpoint); } const socketOnlyEndpointSet = new Set(Object.values(socketOnlyEndpoints)); export function endpointIsSocketOnly(endpoint: Endpoint): boolean { return socketOnlyEndpointSet.has(endpoint); } diff --git a/lib/types/request-types.js b/lib/types/request-types.js index eaa55caa7..0fac62ecf 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,184 +1,184 @@ // @flow import invariant from 'invariant'; import { type ActivityUpdate } from './activity-types'; import type { Shape } from './core'; import type { Platform, PlatformDetails } from './device-types'; import type { RawEntryInfo, CalendarQuery } from './entry-types'; import type { ThreadInconsistencyReportShape, EntryInconsistencyReportShape, ClientThreadInconsistencyReportShape, ClientEntryInconsistencyReportShape, } from './report-types'; import type { RawThreadInfo } from './thread-types'; import type { CurrentUserInfo, OldCurrentUserInfo, AccountUserInfo, } from './user-types'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, MORE_ONE_TIME_KEYS: 8, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || serverRequestType === 8, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; type PlatformClientResponse = { +type: 0, +platform: Platform, }; export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; type PlatformDetailsServerRequest = { type: 3, }; type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Shape<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Shape<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo | OldCurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; type MoreOneTimeKeysServerRequest = { +type: 8, }; type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest | MoreOneTimeKeysServerRequest; export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Shape<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Shape<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest | MoreOneTimeKeysServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; -export type GetUserPublicKeysArgs = { - +userID: string, +export type GetSessionPublicKeysArgs = { + +session: string, }; diff --git a/lib/types/session-types.js b/lib/types/session-types.js index efb41506b..710d8f636 100644 --- a/lib/types/session-types.js +++ b/lib/types/session-types.js @@ -1,104 +1,109 @@ // @flow import type { LogInActionSource } from './account-types'; import type { Shape } from './core'; import type { CalendarQuery } from './entry-types'; import type { RawThreadInfo } from './thread-types'; import { type UserInfo, type CurrentUserInfo, type LoggedOutUserInfo, } from './user-types'; export const cookieLifetime = 30 * 24 * 60 * 60 * 1000; // in milliseconds // Interval the server waits after a state check before starting a new one export const sessionCheckFrequency = 3 * 60 * 1000; // in milliseconds // How long the server debounces after activity before initiating a state check export const stateCheckInactivityActivationInterval = 3 * 1000; // in milliseconds // On native, we specify the cookie directly in the request and response body. // We do this because: // (1) We don't have the same XSS risks as we do on web, so there is no need to // prevent JavaScript from knowing the cookie password. // (2) In the past the internal cookie logic on Android has been buggy. // https://github.com/facebook/react-native/issues/12956 is an example // issue. By specifying the cookie in the body we retain full control of how // that data is passed, without necessitating any native modules like // react-native-cookies. export const cookieSources = Object.freeze({ BODY: 0, HEADER: 1, }); export type CookieSource = $Values; // On native, we use the cookieID as a unique session identifier. This is // because there is no way to have two instances of an app running. On the other // hand, on web it is possible to have two sessions open using the same cookie, // so we have a unique sessionID specified in the request body. export const sessionIdentifierTypes = Object.freeze({ COOKIE_ID: 0, BODY_SESSION_ID: 1, }); export type SessionIdentifierType = $Values; export const cookieTypes = Object.freeze({ USER: 'user', ANONYMOUS: 'anonymous', }); export type CookieType = $Values; export type ServerSessionChange = | { cookieInvalidated: false, threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, sessionID?: null | string, cookie?: string, } | { cookieInvalidated: true, threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, currentUserInfo: LoggedOutUserInfo, sessionID?: null | string, cookie?: string, }; export type ClientSessionChange = | { +cookieInvalidated: false, +currentUserInfo?: ?CurrentUserInfo, +sessionID?: null | string, +cookie?: string, } | { +cookieInvalidated: true, +currentUserInfo: LoggedOutUserInfo, +sessionID?: null | string, +cookie?: string, }; export type PreRequestUserState = { +currentUserInfo: ?CurrentUserInfo, +cookie: ?string, +sessionID: ?string, }; export type SetSessionPayload = { sessionChange: ClientSessionChange, preRequestUserState: ?PreRequestUserState, error: ?string, source: ?LogInActionSource, }; export type SessionState = { calendarQuery: CalendarQuery, messagesCurrentAsOf: number, updatesCurrentAsOf: number, watchedIDs: $ReadOnlyArray, }; export type SessionIdentification = Shape<{ cookie: ?string, sessionID: ?string, }>; + +export type SessionPublicKeys = { + +identityKey: string, + +oneTimeKey?: string, +}; diff --git a/lib/types/user-types.js b/lib/types/user-types.js index 8d63c6150..b65f442ed 100644 --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -1,81 +1,76 @@ // @flow import type { DefaultNotificationPayload } from './account-types'; import type { UserRelationshipStatus } from './relationship-types'; import type { UserInconsistencyReportCreationRequest } from './report-types'; export type GlobalUserInfo = { +id: string, +username: ?string, }; export type GlobalAccountUserInfo = { +id: string, +username: string, }; export type UserInfo = { +id: string, +username: ?string, +relationshipStatus?: UserRelationshipStatus, }; export type UserInfos = { +[id: string]: UserInfo }; export type AccountUserInfo = { +id: string, +username: string, +relationshipStatus?: UserRelationshipStatus, }; export type UserStore = { +userInfos: UserInfos, +inconsistencyReports: $ReadOnlyArray, }; export type RelativeUserInfo = { +id: string, +username: ?string, +isViewer: boolean, }; export type OldLoggedInUserInfo = { +id: string, +username: string, +email: string, +emailVerified: boolean, }; export type LoggedInUserInfo = { +id: string, +username: string, +settings?: DefaultNotificationPayload, }; export type LoggedOutUserInfo = { +id: string, +anonymous: true, }; export type OldCurrentUserInfo = OldLoggedInUserInfo | LoggedOutUserInfo; export type CurrentUserInfo = LoggedInUserInfo | LoggedOutUserInfo; export type PasswordUpdate = { +updatedFields: { +password?: ?string, }, +currentPassword: string, }; export type UserListItem = { +id: string, +username: string, +disabled?: boolean, +notice?: string, +alertText?: string, +alertTitle?: string, }; - -export type UserPublicKeys = { - +identityKey: string, - +oneTimeKey?: string, -}; diff --git a/server/src/creators/one-time-keys-creator.js b/server/src/creators/one-time-keys-creator.js index ef274c76b..63eb4e703 100644 --- a/server/src/creators/one-time-keys-creator.js +++ b/server/src/creators/one-time-keys-creator.js @@ -1,23 +1,26 @@ // @flow import { dbQuery, SQL } from '../database/database'; import type { Viewer } from '../session/viewer'; async function saveOneTimeKeys( viewer: Viewer, oneTimeKeys: $ReadOnlyArray, ): Promise { if (oneTimeKeys.length === 0) { return; } - const insertData = oneTimeKeys.map(oneTimeKey => [viewer.userID, oneTimeKey]); + const insertData = oneTimeKeys.map(oneTimeKey => [ + viewer.session, + oneTimeKey, + ]); const query = SQL` - INSERT INTO one_time_keys(user, one_time_key) + INSERT INTO one_time_keys(session, one_time_key) VALUES ${insertData} `; await dbQuery(query); } export { saveOneTimeKeys }; diff --git a/server/src/deleters/one-time-key-deleters.js b/server/src/deleters/one-time-key-deleters.js index d74406d04..f489fc6c7 100644 --- a/server/src/deleters/one-time-key-deleters.js +++ b/server/src/deleters/one-time-key-deleters.js @@ -1,16 +1,16 @@ // @flow import { dbQuery, SQL } from '../database/database'; async function deleteOneTimeKey( - userID: string, + session: string, oneTimeKey: string, ): Promise { await dbQuery(SQL` DELETE FROM one_time_keys - WHERE user = ${userID} AND one_time_key = ${oneTimeKey} + WHERE session = ${session} AND one_time_key = ${oneTimeKey} `); } export { deleteOneTimeKey }; diff --git a/server/src/endpoints.js b/server/src/endpoints.js index d91c7e73a..016803ca0 100644 --- a/server/src/endpoints.js +++ b/server/src/endpoints.js @@ -1,101 +1,101 @@ // @flow import type { Endpoint } from 'lib/types/endpoints'; import { updateActivityResponder, threadSetUnreadStatusResponder, } from './responders/activity-responders'; import { deviceTokenUpdateResponder } from './responders/device-responders'; import { entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, } from './responders/entry-responders'; import type { JSONResponder } from './responders/handlers'; -import { getUserPublicKeysResponder } from './responders/keys-responders'; +import { getSessionPublicKeysResponder } from './responders/keys-responders'; import { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, } from './responders/message-responders'; import { updateRelationshipsResponder } from './responders/relationship-responders'; import { reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, } from './responders/report-responders'; import { userSearchResponder } from './responders/search-responders'; import { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, } from './responders/thread-responders'; import { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, oldPasswordUpdateResponder, requestAccessResponder, updateUserSettingsResponder, } from './responders/user-responders'; import { codeVerificationResponder } from './responders/verification-responders'; import { uploadDeletionResponder } from './uploads/uploads'; const jsonEndpoints: { [id: Endpoint]: JSONResponder } = { create_account: accountCreationResponder, create_entry: entryCreationResponder, create_error_report: reportCreationResponder, create_multimedia_message: multimediaMessageCreationResponder, create_report: reportCreationResponder, create_reports: reportMultiCreationResponder, create_text_message: textMessageCreationResponder, create_thread: threadCreationResponder, delete_account: accountDeletionResponder, delete_entry: entryDeletionResponder, delete_thread: threadDeletionResponder, delete_upload: uploadDeletionResponder, fetch_entries: entryFetchResponder, fetch_entry_revisions: entryRevisionFetchResponder, fetch_error_report_infos: errorReportFetchInfosResponder, fetch_messages: messageFetchResponder, - get_user_public_keys: getUserPublicKeysResponder, + get_session_public_keys: getSessionPublicKeysResponder, join_thread: threadJoinResponder, leave_thread: threadLeaveResponder, log_in: logInResponder, log_out: logOutResponder, remove_members: memberRemovalResponder, request_access: requestAccessResponder, restore_entry: entryRestorationResponder, search_users: userSearchResponder, send_password_reset_email: sendPasswordResetEmailResponder, send_verification_email: sendVerificationEmailResponder, set_thread_unread_status: threadSetUnreadStatusResponder, update_account: passwordUpdateResponder, update_activity: updateActivityResponder, update_calendar_query: calendarQueryUpdateResponder, update_user_settings: updateUserSettingsResponder, update_device_token: deviceTokenUpdateResponder, update_entry: entryUpdateResponder, update_password: oldPasswordUpdateResponder, update_relationships: updateRelationshipsResponder, update_role: roleUpdateResponder, update_thread: threadUpdateResponder, update_user_subscription: userSubscriptionUpdateResponder, verify_code: codeVerificationResponder, }; export { jsonEndpoints }; diff --git a/server/src/fetchers/key-fetchers.js b/server/src/fetchers/key-fetchers.js index 88cb0fb7a..b853ff119 100644 --- a/server/src/fetchers/key-fetchers.js +++ b/server/src/fetchers/key-fetchers.js @@ -1,55 +1,53 @@ // @flow -import type { UserPublicKeys } from 'lib/types/user-types'; +import type { SessionPublicKeys } from 'lib/types/session-types'; import { minimumOneTimeKeysRequired } from 'lib/utils/crypto-utils'; import { ServerError } from 'lib/utils/errors'; import { dbQuery, SQL } from '../database/database'; import { deleteOneTimeKey } from '../deleters/one-time-key-deleters'; -import type { Viewer } from '../session/viewer'; -async function checkIfUserHasEnoughOneTimeKeys( - userID: string, +async function checkIfSessionHasEnoughOneTimeKeys( + session: string, ): Promise { const query = SQL` SELECT COUNT(*) AS count FROM one_time_keys - WHERE user = ${userID} + WHERE session = ${session} `; const [queryResult] = await dbQuery(query); if (!queryResult.length || queryResult[0].count === undefined) { throw new ServerError('internal_error'); } const [{ count }] = queryResult; return count >= minimumOneTimeKeysRequired; } -async function fetchUserPublicKeys( - viewer: Viewer, - userID: string, -): Promise { +async function fetchSessionPublicKeys( + session: string, +): Promise { const query = SQL` - SELECT u.public_key, otk.one_time_key - FROM users u - LEFT JOIN one_time_keys otk ON otk.user = u.id - WHERE u.id = ${userID} + SELECT s.public_key, otk.one_time_key + FROM sessions s + LEFT JOIN one_time_keys otk ON otk.session = s.id + WHERE s.id = ${session} LIMIT 1 `; const [queryResult] = await dbQuery(query); if (!queryResult.length) { return null; } const [result] = queryResult; if (!result.public_key) { return null; } const oneTimeKey = result.one_time_key; const identityKey = result.public_key; - await deleteOneTimeKey(userID, oneTimeKey); + await deleteOneTimeKey(session, oneTimeKey); return { identityKey, oneTimeKey }; } -export { fetchUserPublicKeys, checkIfUserHasEnoughOneTimeKeys }; +export { fetchSessionPublicKeys, checkIfSessionHasEnoughOneTimeKeys }; diff --git a/server/src/responders/keys-responders.js b/server/src/responders/keys-responders.js index 16f14417f..e5dc449a7 100644 --- a/server/src/responders/keys-responders.js +++ b/server/src/responders/keys-responders.js @@ -1,29 +1,29 @@ // @flow import t from 'tcomb'; -import type { GetUserPublicKeysArgs } from 'lib/types/request-types'; -import type { UserPublicKeys } from 'lib/types/user-types'; +import type { GetSessionPublicKeysArgs } from 'lib/types/request-types'; +import type { SessionPublicKeys } from 'lib/types/session-types'; import { tShape } from 'lib/utils/validation-utils'; -import { fetchUserPublicKeys } from '../fetchers/key-fetchers'; +import { fetchSessionPublicKeys } from '../fetchers/key-fetchers'; import type { Viewer } from '../session/viewer'; import { validateInput } from '../utils/validation-utils'; -const getUserPublicKeysInputValidator = tShape({ - userID: t.String, +const getSessionPublicKeysInputValidator = tShape({ + session: t.String, }); -async function getUserPublicKeysResponder( +async function getSessionPublicKeysResponder( viewer: Viewer, input: any, -): Promise { +): Promise { if (!viewer.loggedIn) { return null; } - const request: GetUserPublicKeysArgs = input; - await validateInput(viewer, getUserPublicKeysInputValidator, request); - return await fetchUserPublicKeys(viewer, request.userID); + const request: GetSessionPublicKeysArgs = input; + await validateInput(viewer, getSessionPublicKeysInputValidator, request); + return await fetchSessionPublicKeys(request.session); } -export { getUserPublicKeysResponder }; +export { getSessionPublicKeysResponder }; diff --git a/server/src/scripts/create-db.js b/server/src/scripts/create-db.js index b09c61040..a0881e9f1 100644 --- a/server/src/scripts/create-db.js +++ b/server/src/scripts/create-db.js @@ -1,406 +1,406 @@ // @flow import ashoat from 'lib/facts/ashoat'; import bots from 'lib/facts/bots'; import genesis from 'lib/facts/genesis'; import { usernameMaxLength } from 'lib/shared/account-utils'; import { sortIDs } from 'lib/shared/relationship-utils'; import { undirectedStatus } from 'lib/types/relationship-types'; import { threadTypes } from 'lib/types/thread-types'; import { createThread } from '../creators/thread-creator'; import { dbQuery, SQL } from '../database/database'; import { createScriptViewer } from '../session/scripts'; import { setScriptContext } from './script-context'; import { endScript } from './utils'; setScriptContext({ allowMultiStatementSQLQueries: true, }); async function main() { try { await createTables(); await createUsers(); await createThreads(); endScript(); } catch (e) { endScript(); console.warn(e); } } async function createTables() { await dbQuery(SQL` CREATE TABLE cookies ( id bigint(20) NOT NULL, hash char(60) NOT NULL, user bigint(20) DEFAULT NULL, platform varchar(255) DEFAULT NULL, creation_time bigint(20) NOT NULL, last_used bigint(20) NOT NULL, device_token varchar(255) DEFAULT NULL, versions json DEFAULT NULL, \`primary\` TINYINT(1) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE days ( id bigint(20) NOT NULL, date date NOT NULL, thread bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE entries ( id bigint(20) NOT NULL, day bigint(20) NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creator bigint(20) NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE focused ( user bigint(20) NOT NULL, session bigint(20) NOT NULL, thread bigint(20) NOT NULL, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE ids ( id bigint(20) NOT NULL, table_name varchar(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE memberships ( thread bigint(20) NOT NULL, user bigint(20) NOT NULL, role bigint(20) NOT NULL, permissions json DEFAULT NULL, permissions_for_children json DEFAULT NULL, creation_time bigint(20) NOT NULL, subscription json NOT NULL, last_message bigint(20) NOT NULL DEFAULT 0, last_read_message bigint(20) NOT NULL DEFAULT 0, sender tinyint(1) UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE messages ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, user bigint(20) NOT NULL, type tinyint(3) UNSIGNED NOT NULL, content mediumtext COLLATE utf8mb4_bin, time bigint(20) NOT NULL, creation varchar(255) COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE notifications ( id bigint(20) NOT NULL, user bigint(20) NOT NULL, thread bigint(20) DEFAULT NULL, message bigint(20) DEFAULT NULL, collapse_key varchar(255) DEFAULT NULL, delivery json NOT NULL, rescinded tinyint(1) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE reports ( id bigint(20) NOT NULL, user bigint(20) NOT NULL, type tinyint(3) UNSIGNED NOT NULL, platform varchar(255) NOT NULL, report json NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE revisions ( id bigint(20) NOT NULL, entry bigint(20) NOT NULL, author bigint(20) NOT NULL, text mediumtext COLLATE utf8mb4_bin NOT NULL, creation_time bigint(20) NOT NULL, session bigint(20) NOT NULL, last_update bigint(20) NOT NULL, deleted tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE roles ( id bigint(20) NOT NULL, thread bigint(20) NOT NULL, name varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, permissions json NOT NULL, creation_time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE sessions ( id bigint(20) NOT NULL, user bigint(20) NOT NULL, cookie bigint(20) NOT NULL, query json NOT NULL, creation_time bigint(20) NOT NULL, last_update bigint(20) NOT NULL, last_validated bigint(20) NOT NULL, public_key char(116) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE threads ( id bigint(20) NOT NULL, type tinyint(3) NOT NULL, name varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, description mediumtext COLLATE utf8mb4_bin, parent_thread_id bigint(20) DEFAULT NULL, containing_thread_id bigint(20) DEFAULT NULL, community bigint(20) DEFAULT NULL, depth int UNSIGNED NOT NULL DEFAULT 0, default_role bigint(20) NOT NULL, creator bigint(20) NOT NULL, creation_time bigint(20) NOT NULL, color char(6) COLLATE utf8mb4_bin NOT NULL, source_message bigint(20) DEFAULT NULL UNIQUE, replies_count int UNSIGNED NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE updates ( id bigint(20) NOT NULL, user bigint(20) NOT NULL, type tinyint(3) UNSIGNED NOT NULL, \`key\` bigint(20) DEFAULT NULL, updater bigint(20) DEFAULT NULL, target bigint(20) DEFAULT NULL, content mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, time bigint(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE uploads ( id bigint(20) NOT NULL, uploader bigint(20) NOT NULL, container bigint(20) DEFAULT NULL, type varchar(255) NOT NULL, filename varchar(255) NOT NULL, mime varchar(255) NOT NULL, content longblob NOT NULL, secret varchar(255) NOT NULL, creation_time bigint(20) NOT NULL, extra json DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE users ( id bigint(20) NOT NULL, username varchar(${usernameMaxLength}) COLLATE utf8mb4_bin NOT NULL, hash char(60) COLLATE utf8mb4_bin NOT NULL, avatar varchar(191) COLLATE utf8mb4_bin DEFAULT NULL, creation_time bigint(20) NOT NULL, public_key char(116) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE relationships_undirected ( user1 bigint(20) NOT NULL, user2 bigint(20) NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE relationships_directed ( user1 bigint(20) NOT NULL, user2 bigint(20) NOT NULL, status tinyint(1) UNSIGNED NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE versions ( id bigint(20) NOT NULL, code_version int(11) NOT NULL, platform varchar(255) NOT NULL, creation_time bigint(20) NOT NULL, deploy_time bigint(20) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE one_time_keys ( session bigint(20) NOT NULL, one_time_key char(43) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE user_messages ( recipient bigint(20) NOT NULL, thread bigint(20) NOT NULL, message bigint(20) NOT NULL, time bigint(20) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE settings ( user bigint(20) NOT NULL, name varchar(255) NOT NULL, data mediumtext COLLATE utf8mb4_bin DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE metadata ( name varchar(255) NOT NULL, data varchar(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE cookies ADD PRIMARY KEY (id), ADD UNIQUE KEY device_token (device_token), ADD KEY user_device_token (user,device_token); ALTER TABLE days ADD PRIMARY KEY (id), ADD UNIQUE KEY date_thread (date,thread) USING BTREE; ALTER TABLE entries ADD PRIMARY KEY (id), ADD UNIQUE KEY creator_creation (creator,creation), ADD KEY day (day); ALTER TABLE focused ADD UNIQUE KEY user_cookie_thread (user,session,thread), ADD KEY thread_user (thread,user); ALTER TABLE ids ADD PRIMARY KEY (id); ALTER TABLE memberships ADD UNIQUE KEY thread_user (thread,user) USING BTREE, ADD KEY role (role) USING BTREE; ALTER TABLE memberships ADD INDEX user (user); ALTER TABLE messages ADD PRIMARY KEY (id), ADD UNIQUE KEY user_creation (user,creation), ADD KEY thread (thread); ALTER TABLE notifications ADD PRIMARY KEY (id), ADD KEY rescinded_user_collapse_key (rescinded,user,collapse_key) USING BTREE, ADD KEY thread (thread), ADD KEY rescinded_user_thread_message (rescinded,user,thread,message) USING BTREE; ALTER TABLE notifications ADD INDEX user (user); ALTER TABLE reports ADD PRIMARY KEY (id); ALTER TABLE revisions ADD PRIMARY KEY (id), ADD KEY entry (entry); ALTER TABLE roles ADD PRIMARY KEY (id), ADD KEY thread (thread); ALTER TABLE sessions ADD PRIMARY KEY (id), - ADD KEY user (user); + ADD KEY user (user), ADD UNIQUE INDEX public_key (public_key); ALTER TABLE threads ADD PRIMARY KEY (id), ADD INDEX parent_thread_id (parent_thread_id), ADD INDEX containing_thread_id (containing_thread_id), ADD INDEX community (community); ALTER TABLE updates ADD PRIMARY KEY (id), ADD INDEX user_time (user,time), ADD INDEX target_time (target, time), ADD INDEX user_key_target_type_time (user, \`key\`, target, type, time), ADD INDEX user_key_type_time (user, \`key\`, type, time), ADD INDEX user_key_time (user, \`key\`, time); ALTER TABLE uploads ADD PRIMARY KEY (id); ALTER TABLE users ADD PRIMARY KEY (id), ADD UNIQUE KEY username (username), ADD UNIQUE INDEX public_key (public_key); ALTER TABLE relationships_undirected ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE relationships_directed ADD UNIQUE KEY user1_user2 (user1,user2), ADD UNIQUE KEY user2_user1 (user2,user1); ALTER TABLE versions ADD PRIMARY KEY (id), ADD UNIQUE KEY code_version_platform (code_version,platform); ALTER TABLE one_time_keys ADD PRIMARY KEY (session, one_time_key); ALTER TABLE user_messages ADD INDEX recipient_time (recipient, time), ADD INDEX recipient_thread_time (recipient, thread, time), ADD INDEX thread (thread), ADD PRIMARY KEY (recipient, message); ALTER TABLE ids MODIFY id bigint(20) NOT NULL AUTO_INCREMENT; ALTER TABLE settings ADD PRIMARY KEY (user, name); ALTER TABLE metadata ADD PRIMARY KEY (name); `); } async function createUsers() { const [user1, user2] = sortIDs(bots.commbot.userID, ashoat.id); await dbQuery(SQL` INSERT INTO ids (id, table_name) VALUES (${bots.commbot.userID}, 'users'), (${ashoat.id}, 'users'); INSERT INTO users (id, username, hash, avatar, creation_time) VALUES (${bots.commbot.userID}, 'commbot', '', NULL, 1530049900980), (${ashoat.id}, 'ashoat', '', NULL, 1463588881886); INSERT INTO relationships_undirected (user1, user2, status) VALUES (${user1}, ${user2}, ${undirectedStatus.KNOW_OF}); `); } const createThreadOptions = { forceAddMembers: true }; async function createThreads() { const insertIDsPromise = dbQuery(SQL` INSERT INTO ids (id, table_name) VALUES (${genesis.id}, 'threads'), (${bots.commbot.staffThreadID}, 'threads'); `); const ashoatViewer = createScriptViewer(ashoat.id); const createGenesisPromise = createThread( ashoatViewer, { id: genesis.id, type: threadTypes.GENESIS, name: genesis.name, description: genesis.description, initialMemberIDs: [bots.commbot.userID], }, createThreadOptions, ); await Promise.all([insertIDsPromise, createGenesisPromise]); const commbotViewer = createScriptViewer(bots.commbot.userID); await createThread( commbotViewer, { id: bots.commbot.staffThreadID, type: threadTypes.COMMUNITY_SECRET_SUBTHREAD, initialMemberIDs: [ashoat.id], }, createThreadOptions, ); } main(); diff --git a/server/src/socket/session-utils.js b/server/src/socket/session-utils.js index 320a90237..8457d7be9 100644 --- a/server/src/socket/session-utils.js +++ b/server/src/socket/session-utils.js @@ -1,556 +1,556 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import type { TUnion, TInterface } from 'tcomb'; import { usersInRawEntryInfos, serverEntryInfo, serverEntryInfosObject, } from 'lib/shared/entry-utils'; import { usersInThreadInfo } from 'lib/shared/thread-utils'; import { hasMinCodeVersion } from 'lib/shared/version-utils'; import type { UpdateActivityResult } from 'lib/types/activity-types'; import { isDeviceType } from 'lib/types/device-types'; import type { CalendarQuery, DeltaEntryInfosResponse, } from 'lib/types/entry-types'; import { reportTypes, type ThreadInconsistencyReportCreationRequest, type EntryInconsistencyReportCreationRequest, } from 'lib/types/report-types'; import { serverRequestTypes, type ThreadInconsistencyClientResponse, type EntryInconsistencyClientResponse, type ClientResponse, type ServerServerRequest, type ServerCheckStateServerRequest, } from 'lib/types/request-types'; import { sessionCheckFrequency } from 'lib/types/session-types'; import { hash } from 'lib/utils/objects'; import { promiseAll } from 'lib/utils/promises'; import { tShape, tPlatform, tPlatformDetails, } from 'lib/utils/validation-utils'; import { saveOneTimeKeys } from '../creators/one-time-keys-creator'; import createReport from '../creators/report-creator'; import { SQL } from '../database/database'; import { fetchEntryInfos, fetchEntryInfosByID, fetchEntriesForSession, } from '../fetchers/entry-fetchers'; -import { checkIfUserHasEnoughOneTimeKeys } from '../fetchers/key-fetchers'; +import { checkIfSessionHasEnoughOneTimeKeys } from '../fetchers/key-fetchers'; import { fetchThreadInfos } from '../fetchers/thread-fetchers'; import { fetchCurrentUserInfo, fetchUserInfos, fetchKnownUserInfos, } from '../fetchers/user-fetchers'; import { activityUpdatesInputValidator } from '../responders/activity-responders'; import { handleAsyncPromise } from '../responders/handlers'; import { threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, } from '../responders/report-responders'; import { setNewSession, setCookiePlatform, setCookiePlatformDetails, } from '../session/cookies'; import type { Viewer } from '../session/viewer'; import { activityUpdater } from '../updaters/activity-updaters'; import { compareNewCalendarQuery } from '../updaters/entry-updaters'; import type { SessionUpdate } from '../updaters/session-updaters'; const clientResponseInputValidator: TUnion = t.union([ tShape({ type: t.irreducible( 'serverRequestTypes.PLATFORM', x => x === serverRequestTypes.PLATFORM, ), platform: tPlatform, }), tShape({ ...threadInconsistencyReportValidatorShape, type: t.irreducible( 'serverRequestTypes.THREAD_INCONSISTENCY', x => x === serverRequestTypes.THREAD_INCONSISTENCY, ), }), tShape({ ...entryInconsistencyReportValidatorShape, type: t.irreducible( 'serverRequestTypes.ENTRY_INCONSISTENCY', x => x === serverRequestTypes.ENTRY_INCONSISTENCY, ), }), tShape({ type: t.irreducible( 'serverRequestTypes.PLATFORM_DETAILS', x => x === serverRequestTypes.PLATFORM_DETAILS, ), platformDetails: tPlatformDetails, }), tShape({ type: t.irreducible( 'serverRequestTypes.CHECK_STATE', x => x === serverRequestTypes.CHECK_STATE, ), hashResults: t.dict(t.String, t.Boolean), }), tShape({ type: t.irreducible( 'serverRequestTypes.INITIAL_ACTIVITY_UPDATES', x => x === serverRequestTypes.INITIAL_ACTIVITY_UPDATES, ), activityUpdates: activityUpdatesInputValidator, }), tShape({ type: t.irreducible( 'serverRequestTypes.MORE_ONE_TIME_KEYS', x => x === serverRequestTypes.MORE_ONE_TIME_KEYS, ), keys: t.list(t.String), }), ]); type StateCheckStatus = | { status: 'state_validated' } | { status: 'state_invalid', invalidKeys: $ReadOnlyArray } | { status: 'state_check' }; type ProcessClientResponsesResult = { serverRequests: ServerServerRequest[], stateCheckStatus: ?StateCheckStatus, activityUpdateResult: ?UpdateActivityResult, }; async function processClientResponses( viewer: Viewer, clientResponses: $ReadOnlyArray, ): Promise { let viewerMissingPlatform = !viewer.platform; const { platformDetails } = viewer; let viewerMissingPlatformDetails = !platformDetails || (isDeviceType(viewer.platform) && (platformDetails.codeVersion === null || platformDetails.codeVersion === undefined || platformDetails.stateVersion === null || platformDetails.stateVersion === undefined)); const promises = []; let activityUpdates = []; let stateCheckStatus = null; const clientSentPlatformDetails = clientResponses.some( response => response.type === serverRequestTypes.PLATFORM_DETAILS, ); for (const clientResponse of clientResponses) { if ( clientResponse.type === serverRequestTypes.PLATFORM && !clientSentPlatformDetails ) { promises.push(setCookiePlatform(viewer, clientResponse.platform)); viewerMissingPlatform = false; if (!isDeviceType(clientResponse.platform)) { viewerMissingPlatformDetails = false; } } else if ( clientResponse.type === serverRequestTypes.THREAD_INCONSISTENCY ) { promises.push(recordThreadInconsistency(viewer, clientResponse)); } else if (clientResponse.type === serverRequestTypes.ENTRY_INCONSISTENCY) { promises.push(recordEntryInconsistency(viewer, clientResponse)); } else if (clientResponse.type === serverRequestTypes.PLATFORM_DETAILS) { promises.push( setCookiePlatformDetails(viewer, clientResponse.platformDetails), ); viewerMissingPlatform = false; viewerMissingPlatformDetails = false; } else if ( clientResponse.type === serverRequestTypes.INITIAL_ACTIVITY_UPDATES ) { activityUpdates = [...activityUpdates, ...clientResponse.activityUpdates]; } else if (clientResponse.type === serverRequestTypes.CHECK_STATE) { const invalidKeys = []; for (const key in clientResponse.hashResults) { const result = clientResponse.hashResults[key]; if (!result) { invalidKeys.push(key); } } stateCheckStatus = invalidKeys.length > 0 ? { status: 'state_invalid', invalidKeys } : { status: 'state_validated' }; } else if (clientResponse.type === serverRequestTypes.MORE_ONE_TIME_KEYS) { invariant(clientResponse.keys, 'keys expected in client response'); handleAsyncPromise(saveOneTimeKeys(viewer, clientResponse.keys)); } } const activityUpdatePromise = (async () => { if (activityUpdates.length === 0) { return; } return await activityUpdater(viewer, { updates: activityUpdates }); })(); const serverRequests = []; const checkOneTimeKeysPromise = (async () => { if (!viewer.loggedIn) { return; } - const enoughOneTimeKeys = await checkIfUserHasEnoughOneTimeKeys( - viewer.userID, + const enoughOneTimeKeys = await checkIfSessionHasEnoughOneTimeKeys( + viewer.session, ); if (!enoughOneTimeKeys) { serverRequests.push({ type: serverRequestTypes.MORE_ONE_TIME_KEYS }); } })(); const { activityUpdateResult } = await promiseAll({ all: Promise.all(promises), activityUpdateResult: activityUpdatePromise, checkOneTimeKeysPromise, }); if ( !stateCheckStatus && viewer.loggedIn && viewer.sessionLastValidated + sessionCheckFrequency < Date.now() ) { stateCheckStatus = { status: 'state_check' }; } if (viewerMissingPlatform) { serverRequests.push({ type: serverRequestTypes.PLATFORM }); } if (viewerMissingPlatformDetails) { serverRequests.push({ type: serverRequestTypes.PLATFORM_DETAILS }); } return { serverRequests, stateCheckStatus, activityUpdateResult }; } async function recordThreadInconsistency( viewer: Viewer, response: ThreadInconsistencyClientResponse, ): Promise { const { type, ...rest } = response; const reportCreationRequest = ({ ...rest, type: reportTypes.THREAD_INCONSISTENCY, }: ThreadInconsistencyReportCreationRequest); await createReport(viewer, reportCreationRequest); } async function recordEntryInconsistency( viewer: Viewer, response: EntryInconsistencyClientResponse, ): Promise { const { type, ...rest } = response; const reportCreationRequest = ({ ...rest, type: reportTypes.ENTRY_INCONSISTENCY, }: EntryInconsistencyReportCreationRequest); await createReport(viewer, reportCreationRequest); } type SessionInitializationResult = | { sessionContinued: false } | { sessionContinued: true, deltaEntryInfoResult: DeltaEntryInfosResponse, sessionUpdate: SessionUpdate, }; async function initializeSession( viewer: Viewer, calendarQuery: CalendarQuery, oldLastUpdate: number, ): Promise { if (!viewer.loggedIn) { return { sessionContinued: false }; } if (oldLastUpdate < viewer.sessionLastUpdated) { // If the client has an older last_update than the server is tracking for // that client, then the client either had some issue persisting its store, // or the user restored the client app from a backup. Either way, we should // invalidate the existing session, since the server has assumed that the // checkpoint is further along than it is on the client, and might not still // have all of the updates necessary to do an incremental update await setNewSession(viewer, calendarQuery, oldLastUpdate); return { sessionContinued: false }; } let comparisonResult = null; try { comparisonResult = compareNewCalendarQuery(viewer, calendarQuery); } catch (e) { if (e.message !== 'unknown_error') { throw e; } } if (comparisonResult) { const { difference, oldCalendarQuery } = comparisonResult; const sessionUpdate = { ...comparisonResult.sessionUpdate, lastUpdate: oldLastUpdate, }; const deltaEntryInfoResult = await fetchEntriesForSession( viewer, difference, oldCalendarQuery, ); return { sessionContinued: true, deltaEntryInfoResult, sessionUpdate }; } else { await setNewSession(viewer, calendarQuery, oldLastUpdate); return { sessionContinued: false }; } } type StateCheckResult = { sessionUpdate?: SessionUpdate, checkStateRequest?: ServerCheckStateServerRequest, }; async function checkState( viewer: Viewer, status: StateCheckStatus, calendarQuery: CalendarQuery, ): Promise { const shouldCheckUserInfos = hasMinCodeVersion(viewer.platformDetails, 59); if (status.status === 'state_validated') { return { sessionUpdate: { lastValidated: Date.now() } }; } else if (status.status === 'state_check') { const promises = { threadsResult: fetchThreadInfos(viewer), entriesResult: fetchEntryInfos(viewer, [calendarQuery]), currentUserInfo: fetchCurrentUserInfo(viewer), userInfosResult: undefined, }; if (shouldCheckUserInfos) { promises.userInfosResult = fetchKnownUserInfos(viewer); } const fetchedData = await promiseAll(promises); let hashesToCheck = { threadInfos: hash(fetchedData.threadsResult.threadInfos), entryInfos: hash( serverEntryInfosObject(fetchedData.entriesResult.rawEntryInfos), ), currentUserInfo: hash(fetchedData.currentUserInfo), }; if (shouldCheckUserInfos) { hashesToCheck = { ...hashesToCheck, userInfos: hash(fetchedData.userInfosResult), }; } const checkStateRequest = { type: serverRequestTypes.CHECK_STATE, hashesToCheck, }; return { checkStateRequest }; } const { invalidKeys } = status; let fetchAllThreads = false, fetchAllEntries = false, fetchAllUserInfos = false, fetchUserInfo = false; const threadIDsToFetch = [], entryIDsToFetch = [], userIDsToFetch = []; for (const key of invalidKeys) { if (key === 'threadInfos') { fetchAllThreads = true; } else if (key === 'entryInfos') { fetchAllEntries = true; } else if (key === 'userInfos') { fetchAllUserInfos = true; } else if (key === 'currentUserInfo') { fetchUserInfo = true; } else if (key.startsWith('threadInfo|')) { const [, threadID] = key.split('|'); threadIDsToFetch.push(threadID); } else if (key.startsWith('entryInfo|')) { const [, entryID] = key.split('|'); entryIDsToFetch.push(entryID); } else if (key.startsWith('userInfo|')) { const [, userID] = key.split('|'); userIDsToFetch.push(userID); } } const fetchPromises = {}; if (fetchAllThreads) { fetchPromises.threadsResult = fetchThreadInfos(viewer); } else if (threadIDsToFetch.length > 0) { fetchPromises.threadsResult = fetchThreadInfos( viewer, SQL`t.id IN (${threadIDsToFetch})`, ); } if (fetchAllEntries) { fetchPromises.entriesResult = fetchEntryInfos(viewer, [calendarQuery]); } else if (entryIDsToFetch.length > 0) { fetchPromises.entryInfos = fetchEntryInfosByID(viewer, entryIDsToFetch); } if (fetchAllUserInfos) { fetchPromises.userInfos = fetchKnownUserInfos(viewer); } else if (userIDsToFetch.length > 0) { fetchPromises.userInfos = fetchKnownUserInfos(viewer, userIDsToFetch); } if (fetchUserInfo) { fetchPromises.currentUserInfo = fetchCurrentUserInfo(viewer); } const fetchedData = await promiseAll(fetchPromises); const hashesToCheck = {}, failUnmentioned = {}, stateChanges = {}; for (const key of invalidKeys) { if (key === 'threadInfos') { // Instead of returning all threadInfos, we want to narrow down and figure // out which threadInfos don't match first const { threadInfos } = fetchedData.threadsResult; for (const threadID in threadInfos) { hashesToCheck[`threadInfo|${threadID}`] = hash(threadInfos[threadID]); } failUnmentioned.threadInfos = true; } else if (key === 'entryInfos') { // Instead of returning all entryInfos, we want to narrow down and figure // out which entryInfos don't match first const { rawEntryInfos } = fetchedData.entriesResult; for (const rawEntryInfo of rawEntryInfos) { const entryInfo = serverEntryInfo(rawEntryInfo); invariant(entryInfo, 'should be set'); const { id: entryID } = entryInfo; invariant(entryID, 'should be set'); hashesToCheck[`entryInfo|${entryID}`] = hash(entryInfo); } failUnmentioned.entryInfos = true; } else if (key === 'userInfos') { // Instead of returning all userInfos, we want to narrow down and figure // out which userInfos don't match first const { userInfos } = fetchedData; for (const userID in userInfos) { hashesToCheck[`userInfo|${userID}`] = hash(userInfos[userID]); } failUnmentioned.userInfos = true; } else if (key === 'currentUserInfo') { stateChanges.currentUserInfo = fetchedData.currentUserInfo; } else if (key.startsWith('threadInfo|')) { const [, threadID] = key.split('|'); const { threadInfos } = fetchedData.threadsResult; const threadInfo = threadInfos[threadID]; if (!threadInfo) { if (!stateChanges.deleteThreadIDs) { stateChanges.deleteThreadIDs = []; } stateChanges.deleteThreadIDs.push(threadID); continue; } if (!stateChanges.rawThreadInfos) { stateChanges.rawThreadInfos = []; } stateChanges.rawThreadInfos.push(threadInfo); } else if (key.startsWith('entryInfo|')) { const [, entryID] = key.split('|'); const rawEntryInfos = fetchedData.entriesResult ? fetchedData.entriesResult.rawEntryInfos : fetchedData.entryInfos; const entryInfo = rawEntryInfos.find( candidate => candidate.id === entryID, ); if (!entryInfo) { if (!stateChanges.deleteEntryIDs) { stateChanges.deleteEntryIDs = []; } stateChanges.deleteEntryIDs.push(entryID); continue; } if (!stateChanges.rawEntryInfos) { stateChanges.rawEntryInfos = []; } stateChanges.rawEntryInfos.push(entryInfo); } else if (key.startsWith('userInfo|')) { const { userInfos: fetchedUserInfos } = fetchedData; const [, userID] = key.split('|'); const userInfo = fetchedUserInfos[userID]; if (!userInfo || !userInfo.username) { if (!stateChanges.deleteUserInfoIDs) { stateChanges.deleteUserInfoIDs = []; } stateChanges.deleteUserInfoIDs.push(userID); } else { if (!stateChanges.userInfos) { stateChanges.userInfos = []; } stateChanges.userInfos.push({ ...userInfo, // Flow gets confused if we don't do this username: userInfo.username, }); } } } if (!shouldCheckUserInfos) { const userIDs = new Set(); if (stateChanges.rawThreadInfos) { for (const threadInfo of stateChanges.rawThreadInfos) { for (const userID of usersInThreadInfo(threadInfo)) { userIDs.add(userID); } } } if (stateChanges.rawEntryInfos) { for (const userID of usersInRawEntryInfos(stateChanges.rawEntryInfos)) { userIDs.add(userID); } } const userInfos = []; if (userIDs.size > 0) { const fetchedUserInfos = await fetchUserInfos([...userIDs]); for (const userID in fetchedUserInfos) { const userInfo = fetchedUserInfos[userID]; if (userInfo && userInfo.username) { const { id, username } = userInfo; userInfos.push({ id, username }); } } } if (userInfos.length > 0) { stateChanges.userInfos = userInfos; } } const checkStateRequest = { type: serverRequestTypes.CHECK_STATE, hashesToCheck, failUnmentioned, stateChanges, }; if (Object.keys(hashesToCheck).length === 0) { return { checkStateRequest, sessionUpdate: { lastValidated: Date.now() } }; } else { return { checkStateRequest }; } } export { clientResponseInputValidator, processClientResponses, initializeSession, checkState, };