diff --git a/keyserver/src/responders/activity-responders.js b/keyserver/src/responders/activity-responders.js index 4be2cb8ee..20908e336 100644 --- a/keyserver/src/responders/activity-responders.js +++ b/keyserver/src/responders/activity-responders.js @@ -1,66 +1,68 @@ // @flow import t from 'tcomb'; import type { TList } from 'tcomb'; import { type UpdateActivityResult, type UpdateActivityRequest, type SetThreadUnreadStatusRequest, type SetThreadUnreadStatusResult, type ActivityUpdate, setThreadUnreadStatusResult, updateActivityResultValidator, } from 'lib/types/activity-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { activityUpdater, setThreadUnreadStatus, } from '../updaters/activity-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; const activityUpdatesInputValidator: TList> = t.list( tShape({ focus: t.Bool, threadID: tID, latestMessage: t.maybe(tID), }), ); -const inputValidator = tShape({ +const inputValidator = tShape({ updates: activityUpdatesInputValidator, }); async function updateActivityResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: UpdateActivityRequest = input; - await validateInput(viewer, inputValidator, request); + const request = await validateInput(viewer, inputValidator, input); const result = await activityUpdater(viewer, request); return validateOutput(viewer, updateActivityResultValidator, result); } const setThreadUnreadStatusValidator = tShape({ threadID: tID, unread: t.Bool, latestMessage: t.maybe(tID), }); async function threadSetUnreadStatusResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SetThreadUnreadStatusRequest = input; - await validateInput(viewer, setThreadUnreadStatusValidator, request); + const request = await validateInput( + viewer, + setThreadUnreadStatusValidator, + input, + ); const result = await setThreadUnreadStatus(viewer, request); return validateOutput(viewer, setThreadUnreadStatusResult, result); } export { activityUpdatesInputValidator, updateActivityResponder, threadSetUnreadStatusResponder, }; diff --git a/keyserver/src/responders/entry-responders.js b/keyserver/src/responders/entry-responders.js index 0d43e8227..35efe6264 100644 --- a/keyserver/src/responders/entry-responders.js +++ b/keyserver/src/responders/entry-responders.js @@ -1,318 +1,341 @@ // @flow import t from 'tcomb'; import type { TInterface } from 'tcomb'; import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js'; import { type CalendarQuery, type SaveEntryRequest, type CreateEntryRequest, type DeleteEntryRequest, type DeleteEntryResponse, type RestoreEntryRequest, type RestoreEntryResponse, type FetchEntryInfosResponse, type DeltaEntryInfosResult, type SaveEntryResponse, rawEntryInfoValidator, } from 'lib/types/entry-types.js'; import { type CalendarFilter, calendarThreadFilterTypes, } from 'lib/types/filter-types.js'; import { type FetchEntryRevisionInfosResult, type FetchEntryRevisionInfosRequest, historyRevisionInfoValidator, } from 'lib/types/history-types.js'; import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import { serverCreateUpdatesResponseValidator } from 'lib/types/update-types.js'; import { accountUserInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tString, tShape, tDate, tID } from 'lib/utils/validation-utils.js'; import createEntry from '../creators/entry-creator.js'; import { deleteEntry, restoreEntry } from '../deleters/entry-deleters.js'; import { fetchEntryInfos, fetchEntryRevisionInfo, fetchEntriesForSession, } from '../fetchers/entry-fetchers.js'; import { verifyThreadIDs } from '../fetchers/thread-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateEntry, compareNewCalendarQuery, } from '../updaters/entry-updaters.js'; import { commitSessionUpdate } from '../updaters/session-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; type EntryQueryInput = { +startDate: string, +endDate: string, +navID?: ?string, +includeDeleted?: ?boolean, +filters?: ?$ReadOnlyArray, }; const entryQueryInputValidator: TInterface = tShape({ navID: t.maybe(t.String), startDate: tDate, endDate: tDate, includeDeleted: t.maybe(t.Boolean), filters: t.maybe( t.list( t.union([ tShape({ type: tString(calendarThreadFilterTypes.NOT_DELETED), }), tShape({ type: tString(calendarThreadFilterTypes.THREAD_LIST), threadIDs: t.list(tID), }), ]), ), ), }); const newEntryQueryInputValidator: TInterface = tShape({ startDate: tDate, endDate: tDate, filters: t.list( t.union([ tShape({ type: tString(calendarThreadFilterTypes.NOT_DELETED), }), tShape({ type: tString(calendarThreadFilterTypes.THREAD_LIST), threadIDs: t.list(tID), }), ]), ), }); function normalizeCalendarQuery(input: any): CalendarQuery { if (input.filters) { return { startDate: input.startDate, endDate: input.endDate, filters: input.filters, }; } const filters = []; if (!input.includeDeleted) { filters.push({ type: calendarThreadFilterTypes.NOT_DELETED }); } if (input.navID !== 'home') { filters.push({ type: calendarThreadFilterTypes.THREAD_LIST, threadIDs: [input.navID], }); } return { startDate: input.startDate, endDate: input.endDate, filters, }; } async function verifyCalendarQueryThreadIDs( request: CalendarQuery, ): Promise { const threadIDsToFilterTo = filteredThreadIDs(request.filters); if (threadIDsToFilterTo && threadIDsToFilterTo.size > 0) { const verifiedThreadIDs = await verifyThreadIDs([...threadIDsToFilterTo]); if (verifiedThreadIDs.length !== threadIDsToFilterTo.size) { throw new ServerError('invalid_parameters'); } } } export const fetchEntryInfosResponseValidator: TInterface = tShape({ rawEntryInfos: t.list(rawEntryInfoValidator), userInfos: t.dict(t.String, accountUserInfoValidator), }); async function entryFetchResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput(viewer, entryQueryInputValidator, input); - const request = normalizeCalendarQuery(input); + const inputQuery = await validateInput( + viewer, + entryQueryInputValidator, + input, + ); + const request = normalizeCalendarQuery(inputQuery); await verifyCalendarQueryThreadIDs(request); const response = await fetchEntryInfos(viewer, [request]); return validateOutput(viewer, fetchEntryInfosResponseValidator, { ...response, userInfos: {}, }); } -const entryRevisionHistoryFetchInputValidator = tShape({ - id: tID, -}); +const entryRevisionHistoryFetchInputValidator = + tShape({ + id: tID, + }); export const fetchEntryRevisionInfosResultValidator: TInterface = tShape({ result: t.list(historyRevisionInfoValidator), }); async function entryRevisionFetchResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: FetchEntryRevisionInfosRequest = input; - await validateInput(viewer, entryRevisionHistoryFetchInputValidator, request); + const request = await validateInput( + viewer, + entryRevisionHistoryFetchInputValidator, + input, + ); const entryHistory = await fetchEntryRevisionInfo(viewer, request.id); const response = { result: entryHistory }; return validateOutput( viewer, fetchEntryRevisionInfosResultValidator, response, ); } -const createEntryRequestInputValidator = tShape({ +const createEntryRequestInputValidator = tShape({ text: t.String, sessionID: t.maybe(t.String), timestamp: t.Number, date: tDate, threadID: tID, localID: t.maybe(t.String), calendarQuery: t.maybe(newEntryQueryInputValidator), }); export const saveEntryResponseValidator: TInterface = tShape({ entryID: tID, newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: serverCreateUpdatesResponseValidator, }); async function entryCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: CreateEntryRequest = input; - await validateInput(viewer, createEntryRequestInputValidator, request); + const request = await validateInput( + viewer, + createEntryRequestInputValidator, + input, + ); const response = await createEntry(viewer, request); return validateOutput(viewer, saveEntryResponseValidator, response); } -const saveEntryRequestInputValidator = tShape({ +const saveEntryRequestInputValidator = tShape({ entryID: tID, text: t.String, prevText: t.String, sessionID: t.maybe(t.String), timestamp: t.Number, calendarQuery: t.maybe(newEntryQueryInputValidator), }); async function entryUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SaveEntryRequest = input; - await validateInput(viewer, saveEntryRequestInputValidator, request); + const request = await validateInput( + viewer, + saveEntryRequestInputValidator, + input, + ); const response = await updateEntry(viewer, request); return validateOutput(viewer, saveEntryResponseValidator, response); } -const deleteEntryRequestInputValidator = tShape({ +const deleteEntryRequestInputValidator = tShape({ entryID: tID, prevText: t.String, sessionID: t.maybe(t.String), timestamp: t.Number, calendarQuery: t.maybe(newEntryQueryInputValidator), }); export const deleteEntryResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, updatesResult: serverCreateUpdatesResponseValidator, }); async function entryDeletionResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: DeleteEntryRequest = input; - await validateInput(viewer, deleteEntryRequestInputValidator, request); + const request = await validateInput( + viewer, + deleteEntryRequestInputValidator, + input, + ); const response = await deleteEntry(viewer, request); return validateOutput(viewer, deleteEntryResponseValidator, response); } -const restoreEntryRequestInputValidator = tShape({ +const restoreEntryRequestInputValidator = tShape({ entryID: tID, sessionID: t.maybe(t.String), timestamp: t.Number, calendarQuery: t.maybe(newEntryQueryInputValidator), }); export const restoreEntryResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: serverCreateUpdatesResponseValidator, }); async function entryRestorationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: RestoreEntryRequest = input; - await validateInput(viewer, restoreEntryRequestInputValidator, request); + const request = await validateInput( + viewer, + restoreEntryRequestInputValidator, + input, + ); const response = await restoreEntry(viewer, request); return validateOutput(viewer, restoreEntryResponseValidator, response); } export const deltaEntryInfosResultValidator: TInterface = tShape({ rawEntryInfos: t.list(rawEntryInfoValidator), deletedEntryIDs: t.list(tID), userInfos: t.list(accountUserInfoValidator), }); async function calendarQueryUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: CalendarQuery = input; - await validateInput(viewer, newEntryQueryInputValidator, input); + const request = await validateInput( + viewer, + newEntryQueryInputValidator, + input, + ); await verifyCalendarQueryThreadIDs(request); if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const { difference, oldCalendarQuery, sessionUpdate } = compareNewCalendarQuery(viewer, request); const [response] = await Promise.all([ fetchEntriesForSession(viewer, difference, oldCalendarQuery), commitSessionUpdate(viewer, sessionUpdate), ]); return validateOutput(viewer, deltaEntryInfosResultValidator, { rawEntryInfos: response.rawEntryInfos, deletedEntryIDs: response.deletedEntryIDs, // Old clients expect userInfos object userInfos: [], }); } export { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, }; diff --git a/keyserver/src/responders/keys-responders.js b/keyserver/src/responders/keys-responders.js index 9696d9de9..11d493b42 100644 --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,41 +1,44 @@ // @flow import t, { type TUnion } from 'tcomb'; import type { GetSessionPublicKeysArgs } from 'lib/types/request-types.js'; import { type SessionPublicKeys, sessionPublicKeysValidator, } from 'lib/types/session-types.js'; import { tShape, tNull } from 'lib/utils/validation-utils.js'; import { fetchSessionPublicKeys } from '../fetchers/key-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const getSessionPublicKeysInputValidator = tShape({ +const getSessionPublicKeysInputValidator = tShape({ session: t.String, }); type GetSessionPublicKeysResponse = SessionPublicKeys | null; export const getSessionPublicKeysResponseValidator: TUnion = t.union([sessionPublicKeysValidator, tNull]); async function getSessionPublicKeysResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { if (!viewer.loggedIn) { return null; } - const request: GetSessionPublicKeysArgs = input; - await validateInput(viewer, getSessionPublicKeysInputValidator, request); + const request = await validateInput( + viewer, + getSessionPublicKeysInputValidator, + input, + ); const response = await fetchSessionPublicKeys(request.session); return validateOutput( viewer, getSessionPublicKeysResponseValidator, response, ); } export { getSessionPublicKeysResponder }; diff --git a/keyserver/src/responders/message-report-responder.js b/keyserver/src/responders/message-report-responder.js index 6986d6cfb..bbd9f474c 100644 --- a/keyserver/src/responders/message-report-responder.js +++ b/keyserver/src/responders/message-report-responder.js @@ -1,39 +1,39 @@ // @flow import type { TInterface } from 'tcomb'; import { type MessageReportCreationRequest, type MessageReportCreationResult, } from 'lib/types/message-report-types.js'; import { rawMessageInfoValidator } from 'lib/types/message-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; import createMessageReport from '../creators/message-report-creator.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const messageReportCreationRequestInputValidator = tShape({ - messageID: tID, -}); +const messageReportCreationRequestInputValidator = + tShape({ + messageID: tID, + }); export const messageReportCreationResultValidator: TInterface = tShape({ messageInfo: rawMessageInfoValidator }); async function messageReportCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput( + const request = await validateInput( viewer, messageReportCreationRequestInputValidator, input, ); - const request: MessageReportCreationRequest = input; const rawMessageInfos = await createMessageReport(viewer, request); const result = { messageInfo: rawMessageInfos[0] }; return validateOutput(viewer, messageReportCreationResultValidator, result); } export { messageReportCreationResponder }; diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js index 05754e148..23e2d6d5b 100644 --- a/keyserver/src/responders/message-responders.js +++ b/keyserver/src/responders/message-responders.js @@ -1,447 +1,462 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { onlyOneEmojiRegex } from 'lib/shared/emojis.js'; import { createMediaMessageData, trimMessage, } from 'lib/shared/message-utils.js'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils.js'; import type { Media } from 'lib/types/media-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { type SendTextMessageRequest, type SendMultimediaMessageRequest, type SendReactionMessageRequest, type SendEditMessageRequest, type FetchMessageInfosResponse, type FetchMessageInfosRequest, defaultNumberPerThread, type SendMessageResponse, type SendEditMessageResponse, type FetchPinnedMessagesRequest, type FetchPinnedMessagesResult, messageTruncationStatusesValidator, rawMessageInfoValidator, } from 'lib/types/message-types.js'; import type { EditMessageData } from 'lib/types/messages/edit.js'; import type { ReactionMessageData } from 'lib/types/messages/reaction.js'; import type { TextMessageData } from 'lib/types/messages/text.js'; import { threadPermissions } from 'lib/types/thread-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { tRegex, tShape, tMediaMessageMedia, tID, } from 'lib/utils/validation-utils.js'; import createMessages from '../creators/message-creator.js'; import { SQL } from '../database/database.js'; import { fetchMessageInfos, fetchMessageInfoForLocalID, fetchMessageInfoByID, fetchThreadMessagesCount, fetchPinnedMessageInfos, } from '../fetchers/message-fetchers.js'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { fetchImages, fetchMediaFromMediaMessageContent, } from '../fetchers/upload-fetchers.js'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { assignImages, assignMessageContainerToMedia, } from '../updaters/upload-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const sendTextMessageRequestInputValidator = tShape({ +const sendTextMessageRequestInputValidator = tShape({ threadID: tID, localID: t.maybe(t.String), text: t.String, sidebarCreation: t.maybe(t.Boolean), }); export const sendMessageResponseValidator: TInterface = tShape({ newMessageInfo: rawMessageInfoValidator }); async function textMessageCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SendTextMessageRequest = input; - await validateInput(viewer, sendTextMessageRequestInputValidator, request); + const request = await validateInput( + viewer, + sendTextMessageRequestInputValidator, + input, + ); const { threadID, localID, text: rawText, sidebarCreation } = request; const text = trimMessage(rawText); if (!text) { throw new ServerError('invalid_parameters'); } const hasPermission = await checkThreadPermission( viewer, threadID, threadPermissions.VOICED, ); if (!hasPermission) { throw new ServerError('invalid_parameters'); } let messageData: TextMessageData = { type: messageTypes.TEXT, threadID, creatorID: viewer.id, time: Date.now(), text, }; if (localID) { messageData = { ...messageData, localID }; } if (sidebarCreation) { const numMessages = await fetchThreadMessagesCount(threadID); if (numMessages === 2) { // sidebarCreation is set below to prevent double notifs from a sidebar // creation. We expect precisely two messages to appear before a // sidebarCreation: a SIDEBAR_SOURCE and a CREATE_SIDEBAR. If two users // attempt to create a sidebar at the same time, then both clients will // attempt to set sidebarCreation here, but we only want to suppress // notifs for the client that won the race. messageData = { ...messageData, sidebarCreation }; } } const rawMessageInfos = await createMessages(viewer, [messageData]); const response = { newMessageInfo: rawMessageInfos[0] }; return validateOutput(viewer, sendMessageResponseValidator, response); } -const fetchMessageInfosRequestInputValidator = tShape({ - cursors: t.dict(tID, t.maybe(tID)), - numberPerThread: t.maybe(t.Number), -}); +const fetchMessageInfosRequestInputValidator = tShape( + { + cursors: t.dict(tID, t.maybe(tID)), + numberPerThread: t.maybe(t.Number), + }, +); export const fetchMessageInfosResponseValidator: TInterface = tShape({ rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function messageFetchResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: FetchMessageInfosRequest = input; - await validateInput(viewer, fetchMessageInfosRequestInputValidator, request); + const request = await validateInput( + viewer, + fetchMessageInfosRequestInputValidator, + input, + ); const response = await fetchMessageInfos( viewer, { threadCursors: request.cursors }, request.numberPerThread ? request.numberPerThread : defaultNumberPerThread, ); return validateOutput(viewer, fetchMessageInfosResponseValidator, { ...response, userInfos: {}, }); } -const sendMultimediaMessageRequestInputValidator = t.union([ - // This option is only used for messageTypes.IMAGES - tShape({ - threadID: tID, - localID: t.String, - sidebarCreation: t.maybe(t.Boolean), - mediaIDs: t.list(tID), - }), - tShape({ - threadID: tID, - localID: t.String, - sidebarCreation: t.maybe(t.Boolean), - mediaMessageContents: t.list(tMediaMessageMedia), - }), -]); +const sendMultimediaMessageRequestInputValidator = + t.union([ + // This option is only used for messageTypes.IMAGES + tShape({ + threadID: tID, + localID: t.String, + sidebarCreation: t.maybe(t.Boolean), + mediaIDs: t.list(tID), + }), + tShape({ + threadID: tID, + localID: t.String, + sidebarCreation: t.maybe(t.Boolean), + mediaMessageContents: t.list(tMediaMessageMedia), + }), + ]); async function multimediaMessageCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SendMultimediaMessageRequest = input; - await validateInput( + const request = await validateInput( viewer, sendMultimediaMessageRequestInputValidator, - request, + input, ); if ( (request.mediaIDs && request.mediaIDs.length === 0) || (request.mediaMessageContents && request.mediaMessageContents.length === 0) ) { throw new ServerError('invalid_parameters'); } const { threadID, localID, sidebarCreation } = request; const hasPermission = await checkThreadPermission( viewer, threadID, threadPermissions.VOICED, ); if (!hasPermission) { throw new ServerError('invalid_parameters'); } const existingMessageInfoPromise = fetchMessageInfoForLocalID( viewer, localID, ); const mediaPromise: Promise<$ReadOnlyArray> = request.mediaIDs ? fetchImages(viewer, request.mediaIDs) : fetchMediaFromMediaMessageContent(viewer, request.mediaMessageContents); const [existingMessageInfo, media] = await Promise.all([ existingMessageInfoPromise, mediaPromise, ]); if (media.length === 0 && !existingMessageInfo) { throw new ServerError('invalid_parameters'); } // We use the MULTIMEDIA type for encrypted photos const containsEncryptedMedia = media.some( m => m.type === 'encrypted_photo' || m.type === 'encrypted_video', ); const messageData = createMediaMessageData( { localID, threadID, creatorID: viewer.id, media, sidebarCreation, }, { forceMultimediaMessageType: containsEncryptedMedia }, ); const [newMessageInfo] = await createMessages(viewer, [messageData]); const { id } = newMessageInfo; invariant( id !== null && id !== undefined, 'serverID should be set in createMessages result', ); if (request.mediaIDs) { await assignImages(viewer, request.mediaIDs, id, threadID); } else { await assignMessageContainerToMedia( viewer, request.mediaMessageContents, id, threadID, ); } const response = { newMessageInfo }; return validateOutput(viewer, sendMessageResponseValidator, response); } -const sendReactionMessageRequestInputValidator = tShape({ - threadID: tID, - localID: t.maybe(t.String), - targetMessageID: tID, - reaction: tRegex(onlyOneEmojiRegex), - action: t.enums.of(['add_reaction', 'remove_reaction']), -}); +const sendReactionMessageRequestInputValidator = + tShape({ + threadID: tID, + localID: t.maybe(t.String), + targetMessageID: tID, + reaction: tRegex(onlyOneEmojiRegex), + action: t.enums.of(['add_reaction', 'remove_reaction']), + }); async function reactionMessageCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SendReactionMessageRequest = input; - await validateInput(viewer, sendReactionMessageRequestInputValidator, input); + const request = await validateInput( + viewer, + sendReactionMessageRequestInputValidator, + input, + ); const { threadID, localID, targetMessageID, reaction, action } = request; if (!targetMessageID || !reaction) { throw new ServerError('invalid_parameters'); } const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID); if (!targetMessageInfo || !targetMessageInfo.id) { throw new ServerError('invalid_parameters'); } const [serverThreadInfos, hasPermission, targetMessageUserInfos] = await Promise.all([ fetchServerThreadInfos(SQL`t.id = ${threadID}`), checkThreadPermission( viewer, threadID, threadPermissions.REACT_TO_MESSAGE, ), fetchKnownUserInfos(viewer, [targetMessageInfo.creatorID]), ]); const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID]; if (targetMessageThreadInfo.sourceMessageID === targetMessageID) { throw new ServerError('invalid_parameters'); } const targetMessageCreator = targetMessageUserInfos[targetMessageInfo.creatorID]; const targetMessageCreatorRelationship = targetMessageCreator?.relationshipStatus; const creatorRelationshipHasBlock = targetMessageCreatorRelationship && relationshipBlockedInEitherDirection(targetMessageCreatorRelationship); if (!hasPermission || creatorRelationshipHasBlock) { throw new ServerError('invalid_parameters'); } let messageData: ReactionMessageData = { type: messageTypes.REACTION, threadID, creatorID: viewer.id, time: Date.now(), targetMessageID, reaction, action, }; if (localID) { messageData = { ...messageData, localID }; } const rawMessageInfos = await createMessages(viewer, [messageData]); const response = { newMessageInfo: rawMessageInfos[0] }; return validateOutput(viewer, sendMessageResponseValidator, response); } -const editMessageRequestInputValidator = tShape({ +const editMessageRequestInputValidator = tShape({ targetMessageID: tID, text: t.String, }); export const sendEditMessageResponseValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), }); async function editMessageCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SendEditMessageRequest = input; - await validateInput(viewer, editMessageRequestInputValidator, input); + const request = await validateInput( + viewer, + editMessageRequestInputValidator, + input, + ); const { targetMessageID, text: rawText } = request; const text = trimMessage(rawText); if (!targetMessageID || !text) { throw new ServerError('invalid_parameters'); } const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID); if (!targetMessageInfo || !targetMessageInfo.id) { throw new ServerError('invalid_parameters'); } if (targetMessageInfo.type !== messageTypes.TEXT) { throw new ServerError('invalid_parameters'); } const { threadID } = targetMessageInfo; const [serverThreadInfos, hasPermission, rawSidebarThreadInfos] = await Promise.all([ fetchServerThreadInfos(SQL`t.id = ${threadID}`), checkThreadPermission(viewer, threadID, threadPermissions.EDIT_MESSAGE), fetchServerThreadInfos( SQL`t.parent_thread_id = ${threadID} AND t.source_message = ${targetMessageID}`, ), ]); const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID]; if (targetMessageThreadInfo.sourceMessageID === targetMessageID) { // We are editing first message of the sidebar // If client wants to do that it sends id of the sourceMessage instead throw new ServerError('invalid_parameters'); } if (!hasPermission) { throw new ServerError('invalid_parameters'); } if (targetMessageInfo.creatorID !== viewer.id) { throw new ServerError('invalid_parameters'); } const time = Date.now(); const messagesData = []; let messageData: EditMessageData = { type: messageTypes.EDIT_MESSAGE, threadID, creatorID: viewer.id, time, targetMessageID, text, }; messagesData.push(messageData); const sidebarThreadValues = values(rawSidebarThreadInfos.threadInfos); for (const sidebarThreadValue of sidebarThreadValues) { if (sidebarThreadValue && sidebarThreadValue.id) { messageData = { type: messageTypes.EDIT_MESSAGE, threadID: sidebarThreadValue.id, creatorID: viewer.id, time, targetMessageID, text: text, }; messagesData.push(messageData); } } const newMessageInfos = await createMessages(viewer, messagesData); const response = { newMessageInfos }; return validateOutput(viewer, sendEditMessageResponseValidator, response); } -const fetchPinnedMessagesResponderInputValidator = tShape({ - threadID: tID, -}); +const fetchPinnedMessagesResponderInputValidator = + tShape({ + threadID: tID, + }); export const fetchPinnedMessagesResultValidator: TInterface = tShape({ pinnedMessages: t.list(rawMessageInfoValidator), }); async function fetchPinnedMessagesResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: FetchPinnedMessagesRequest = input; - await validateInput( + const request = await validateInput( viewer, fetchPinnedMessagesResponderInputValidator, input, ); const response = await fetchPinnedMessageInfos(viewer, request); return validateOutput(viewer, fetchPinnedMessagesResultValidator, response); } export { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, }; diff --git a/keyserver/src/responders/relationship-responders.js b/keyserver/src/responders/relationship-responders.js index 95e1829bd..fe4fcf516 100644 --- a/keyserver/src/responders/relationship-responders.js +++ b/keyserver/src/responders/relationship-responders.js @@ -1,38 +1,41 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type RelationshipRequest, type RelationshipErrors, relationshipActionsList, } from 'lib/types/relationship-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import type { Viewer } from '../session/viewer.js'; import { updateRelationships } from '../updaters/relationship-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const updateRelationshipInputValidator = tShape({ +const updateRelationshipInputValidator = tShape({ action: t.enums.of(relationshipActionsList, 'relationship action'), userIDs: t.list(t.String), }); export const relationshipErrorsValidator: TInterface = tShape({ invalid_user: t.maybe(t.list(t.String)), already_friends: t.maybe(t.list(t.String)), user_blocked: t.maybe(t.list(t.String)), }); async function updateRelationshipsResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: RelationshipRequest = input; - await validateInput(viewer, updateRelationshipInputValidator, request); + const request = await validateInput( + viewer, + updateRelationshipInputValidator, + input, + ); const response = await updateRelationships(viewer, request); return validateOutput(viewer, relationshipErrorsValidator, response); } export { updateRelationshipsResponder }; diff --git a/keyserver/src/responders/report-responders.js b/keyserver/src/responders/report-responders.js index f5669a4c1..a320f3d4c 100644 --- a/keyserver/src/responders/report-responders.js +++ b/keyserver/src/responders/report-responders.js @@ -1,259 +1,265 @@ // @flow import type { $Response, $Request } from 'express'; import t from 'tcomb'; import type { TInterface, TStructProps } from 'tcomb'; import { type ReportCreationResponse, type ReportCreationRequest, type FetchErrorReportInfosResponse, type FetchErrorReportInfosRequest, type ThreadInconsistencyReportShape, type EntryInconsistencyReportShape, reportTypes, reportInfoValidator, } from 'lib/types/report-types.js'; import { userInfoValidator } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tShape, tPlatform, tPlatformDetails, } from 'lib/utils/validation-utils.js'; import { newEntryQueryInputValidator } from './entry-responders.js'; import createReport from '../creators/report-creator.js'; import { fetchErrorReportInfos, fetchReduxToolsImport, } from '../fetchers/report-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; const tActionSummary = tShape({ type: t.String, time: t.Number, summary: t.String, }); const threadInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, pollResult: t.maybe(t.Object), pushResult: t.Object, lastActionTypes: t.maybe(t.list(t.String)), lastActions: t.maybe(t.list(tActionSummary)), time: t.maybe(t.Number), }; const entryInconsistencyReportValidatorShape: TStructProps = { platformDetails: tPlatformDetails, beforeAction: t.Object, action: t.Object, calendarQuery: newEntryQueryInputValidator, pollResult: t.maybe(t.Object), pushResult: t.Object, lastActionTypes: t.maybe(t.list(t.String)), lastActions: t.maybe(t.list(tActionSummary)), time: t.Number, }; const userInconsistencyReportValidatorShape = { platformDetails: tPlatformDetails, action: t.Object, beforeStateCheck: t.Object, afterStateCheck: t.Object, lastActions: t.list(tActionSummary), time: t.Number, }; const threadInconsistencyReportCreationRequest = tShape({ ...threadInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.THREAD_INCONSISTENCY', x => x === reportTypes.THREAD_INCONSISTENCY, ), }); const entryInconsistencyReportCreationRquest = tShape({ ...entryInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.ENTRY_INCONSISTENCY', x => x === reportTypes.ENTRY_INCONSISTENCY, ), }); const mediaMissionReportCreationRequest = tShape({ type: t.irreducible( 'reportTypes.MEDIA_MISSION', x => x === reportTypes.MEDIA_MISSION, ), platformDetails: tPlatformDetails, time: t.Number, mediaMission: t.Object, uploadServerID: t.maybe(t.String), uploadLocalID: t.maybe(t.String), mediaLocalID: t.maybe(t.String), messageServerID: t.maybe(t.String), messageLocalID: t.maybe(t.String), }); const userInconsistencyReportCreationRequest = tShape({ ...userInconsistencyReportValidatorShape, type: t.irreducible( 'reportTypes.USER_INCONSISTENCY', x => x === reportTypes.USER_INCONSISTENCY, ), }); -const reportCreationRequestInputValidator = t.union([ +const reportCreationRequestInputValidator = t.union([ tShape({ type: t.maybe( t.irreducible('reportTypes.ERROR', x => x === reportTypes.ERROR), ), platformDetails: t.maybe(tPlatformDetails), deviceType: t.maybe(tPlatform), codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), errors: t.list( tShape({ errorMessage: t.String, stack: t.maybe(t.String), componentStack: t.maybe(t.String), }), ), preloadedState: t.Object, currentState: t.Object, actions: t.list(t.union([t.Object, t.String])), }), threadInconsistencyReportCreationRequest, entryInconsistencyReportCreationRquest, mediaMissionReportCreationRequest, userInconsistencyReportCreationRequest, ]); export const reportCreationResponseValidator: TInterface = tShape({ id: t.String }); async function reportCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput(viewer, reportCreationRequestInputValidator, input); - if (input.type === null || input.type === undefined) { - input.type = reportTypes.ERROR; + let request = await validateInput( + viewer, + reportCreationRequestInputValidator, + input, + ); + if (request.type === null || request.type === undefined) { + request.type = reportTypes.ERROR; } - if (!input.platformDetails && input.deviceType) { - const { deviceType, codeVersion, stateVersion, ...rest } = input; - input = { + if (!request.platformDetails && request.deviceType) { + const { deviceType, codeVersion, stateVersion, ...rest } = request; + request = { ...rest, platformDetails: { platform: deviceType, codeVersion, stateVersion }, }; } - const request: ReportCreationRequest = input; const response = await createReport(viewer, request); if (!response) { throw new ServerError('ignored_report'); } return validateOutput(viewer, reportCreationResponseValidator, response); } -const reportMultiCreationRequestInputValidator = tShape({ - reports: t.list( - t.union([ - tShape({ - type: t.irreducible('reportTypes.ERROR', x => x === reportTypes.ERROR), - platformDetails: tPlatformDetails, - errors: t.list( - tShape({ - errorMessage: t.String, - stack: t.maybe(t.String), - componentStack: t.maybe(t.String), - }), - ), - preloadedState: t.Object, - currentState: t.Object, - actions: t.list(t.union([t.Object, t.String])), - }), - threadInconsistencyReportCreationRequest, - entryInconsistencyReportCreationRquest, - mediaMissionReportCreationRequest, - userInconsistencyReportCreationRequest, - ]), - ), -}); +const reportMultiCreationRequestInputValidator = + tShape({ + reports: t.list( + t.union([ + tShape({ + type: t.irreducible( + 'reportTypes.ERROR', + x => x === reportTypes.ERROR, + ), + platformDetails: tPlatformDetails, + errors: t.list( + tShape({ + errorMessage: t.String, + stack: t.maybe(t.String), + componentStack: t.maybe(t.String), + }), + ), + preloadedState: t.Object, + currentState: t.Object, + actions: t.list(t.union([t.Object, t.String])), + }), + threadInconsistencyReportCreationRequest, + entryInconsistencyReportCreationRquest, + mediaMissionReportCreationRequest, + userInconsistencyReportCreationRequest, + ]), + ), + }); type ReportMultiCreationRequest = { reports: $ReadOnlyArray, }; async function reportMultiCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ReportMultiCreationRequest = input; - await validateInput( + const request = await validateInput( viewer, reportMultiCreationRequestInputValidator, - request, + input, ); await Promise.all( request.reports.map(reportCreationRequest => createReport(viewer, reportCreationRequest), ), ); } -const fetchErrorReportInfosRequestInputValidator = tShape({ - cursor: t.maybe(t.String), -}); +const fetchErrorReportInfosRequestInputValidator = + tShape({ + cursor: t.maybe(t.String), + }); export const fetchErrorReportInfosResponseValidator: TInterface = tShape({ reports: t.list(reportInfoValidator), userInfos: t.list(userInfoValidator), }); async function errorReportFetchInfosResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: FetchErrorReportInfosRequest = input; - await validateInput( + const request = await validateInput( viewer, fetchErrorReportInfosRequestInputValidator, - request, + input, ); const response = await fetchErrorReportInfos(viewer, request); return validateOutput( viewer, fetchErrorReportInfosResponseValidator, response, ); } async function errorReportDownloadResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const id = req.params.reportID; if (!id) { throw new ServerError('invalid_parameters'); } const result = await fetchReduxToolsImport(viewer, id); res.set('Content-Disposition', `attachment; filename=report-${id}.json`); res.json({ preloadedState: JSON.stringify(result.preloadedState), payload: JSON.stringify(result.payload), }); } export { threadInconsistencyReportValidatorShape, entryInconsistencyReportValidatorShape, reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, errorReportDownloadResponder, }; diff --git a/keyserver/src/responders/search-responders.js b/keyserver/src/responders/search-responders.js index 3762b7151..dffe0bdda 100644 --- a/keyserver/src/responders/search-responders.js +++ b/keyserver/src/responders/search-responders.js @@ -1,36 +1,39 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { UserSearchRequest, UserSearchResult, } from 'lib/types/search-types.js'; import { globalAccountUserInfoValidator } from 'lib/types/user-types.js'; import { tShape } from 'lib/utils/validation-utils.js'; import { searchForUsers } from '../search/users.js'; import type { Viewer } from '../session/viewer.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const userSearchRequestInputValidator = tShape({ +const userSearchRequestInputValidator = tShape({ prefix: t.maybe(t.String), }); export const userSearchResultValidator: TInterface = tShape({ userInfos: t.list(globalAccountUserInfoValidator), }); async function userSearchResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: UserSearchRequest = input; - await validateInput(viewer, userSearchRequestInputValidator, request); + const request = await validateInput( + viewer, + userSearchRequestInputValidator, + input, + ); const searchResults = await searchForUsers(request); const result = { userInfos: searchResults }; return validateOutput(viewer, userSearchResultValidator, result); } export { userSearchResponder }; diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index 92d3418c7..a72e5aa1c 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,303 +1,330 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; import { rawEntryInfoValidator } from 'lib/types/entry-types.js'; import { mediaValidator } from 'lib/types/media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, threadTypes, rawThreadInfoValidator, } from 'lib/types/thread-types.js'; import { serverUpdateInfoValidator } from 'lib/types/update-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const threadDeletionRequestInputValidator = tShape({ +const threadDeletionRequestInputValidator = tShape({ threadID: tID, accountPassword: t.maybe(tPassword), }); export const leaveThreadResultValidator: TInterface = tShape({ threadInfos: t.maybe(t.dict(tID, rawThreadInfoValidator)), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function threadDeletionResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ThreadDeletionRequest = input; - await validateInput(viewer, threadDeletionRequestInputValidator, request); + const request = await validateInput( + viewer, + threadDeletionRequestInputValidator, + input, + ); const result = await deleteThread(viewer, request); return validateOutput(viewer, leaveThreadResultValidator, result); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadInfo: t.maybe(rawThreadInfoValidator), threadInfos: t.maybe(t.dict(tID, rawThreadInfoValidator)), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: RoleChangeRequest = input; - await validateInput(viewer, roleChangeRequestInputValidator, request); + const request = await validateInput( + viewer, + roleChangeRequestInputValidator, + input, + ); const result = await updateRole(viewer, request); return validateOutput(viewer, changeThreadSettingsResultValidator, result); } -const removeMembersRequestInputValidator = tShape({ +const removeMembersRequestInputValidator = tShape({ threadID: tID, memberIDs: t.list(t.String), }); async function memberRemovalResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: RemoveMembersRequest = input; - await validateInput(viewer, removeMembersRequestInputValidator, request); + const request = await validateInput( + viewer, + removeMembersRequestInputValidator, + input, + ); const result = await removeMembers(viewer, request); return validateOutput(viewer, changeThreadSettingsResultValidator, result); } -const leaveThreadRequestInputValidator = tShape({ +const leaveThreadRequestInputValidator = tShape({ threadID: tID, }); async function threadLeaveResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: LeaveThreadRequest = input; - await validateInput(viewer, leaveThreadRequestInputValidator, request); + const request = await validateInput( + viewer, + leaveThreadRequestInputValidator, + input, + ); const result = await leaveThread(viewer, request); return validateOutput(viewer, leaveThreadResultValidator, result); } -const updateThreadRequestInputValidator = tShape({ +const updateThreadRequestInputValidator = tShape({ threadID: tID, changes: tShape({ type: t.maybe(tNumEnum(values(threadTypes))), name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), newMemberIDs: t.maybe(t.list(t.String)), avatar: t.maybe(updateUserAvatarRequestValidator), }), accountPassword: t.maybe(tPassword), }); async function threadUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: UpdateThreadRequest = input; - await validateInput(viewer, updateThreadRequestInputValidator, request); + const request = await validateInput( + viewer, + updateThreadRequestInputValidator, + input, + ); const result = await updateThread(viewer, request); return validateOutput(viewer, changeThreadSettingsResultValidator, result); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), initialMemberIDs: t.maybe(t.list(t.String)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, ]), ...threadRequestValidationShape, }), ]); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), newThreadInfo: t.maybe(rawThreadInfoValidator), userInfos: userInfosValidator, newThreadID: t.maybe(tID), }); async function threadCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ServerNewThreadRequest = input; - await validateInput(viewer, newThreadRequestInputValidator, request); + const request = await validateInput( + viewer, + newThreadRequestInputValidator, + input, + ); const result = await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); return validateOutput(viewer, newThreadResponseValidator, result); } -const joinThreadRequestInputValidator = tShape({ +const joinThreadRequestInputValidator = tShape({ threadID: tID, calendarQuery: t.maybe(entryQueryInputValidator), inviteLinkSecret: t.maybe(t.String), }); export const threadJoinResultValidator: TInterface = tShape({ threadInfos: t.maybe(t.dict(tID, rawThreadInfoValidator)), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), }); async function threadJoinResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ServerThreadJoinRequest = input; - await validateInput(viewer, joinThreadRequestInputValidator, request); + const request = await validateInput( + viewer, + joinThreadRequestInputValidator, + input, + ); if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } const result = await joinThread(viewer, request); return validateOutput(viewer, threadJoinResultValidator, result); } -const threadFetchMediaRequestInputValidator = tShape({ +const threadFetchMediaRequestInputValidator = tShape({ threadID: tID, limit: t.Number, offset: t.Number, }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); async function threadFetchMediaResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ThreadFetchMediaRequest = input; - await validateInput(viewer, threadFetchMediaRequestInputValidator, request); + const request = await validateInput( + viewer, + threadFetchMediaRequestInputValidator, + input, + ); const result = await fetchMediaForThread(viewer, request); return validateOutput(viewer, threadFetchMediaResultValidator, result); } -const toggleMessagePinRequestInputValidator = tShape({ +const toggleMessagePinRequestInputValidator = tShape({ messageID: tID, action: t.enums.of(['pin', 'unpin']), }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); async function toggleMessagePinResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ToggleMessagePinRequest = input; - await validateInput(viewer, toggleMessagePinRequestInputValidator, request); + const request = await validateInput( + viewer, + toggleMessagePinRequestInputValidator, + input, + ); const result = await toggleMessagePinForThread(viewer, request); return validateOutput(viewer, toggleMessagePinResultValidator, result); } export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, }; diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js index c3b065881..d4389fb79 100644 --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,737 +1,774 @@ // @flow import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { ErrorTypes, SiweMessage } from 'siwe'; import t, { type TInterface } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { baseLegalPolicies, policies, policyTypeValidator, } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { ResetPasswordRequest, LogOutResponse, DeleteAccountRequest, RegisterResponse, RegisterRequest, LogInResponse, LogInRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, } from 'lib/types/account-types.js'; import { userSettingsTypes, notificationTypeValues, logInActionSources, } from 'lib/types/account-types.js'; import { type ClientAvatar, clientAvatarValidator, - type UpdateUserAvatarRequest, type UpdateUserAvatarResponse, } from 'lib/types/avatar-types.js'; import type { IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; import { type CalendarQuery, rawEntryInfoValidator, } from 'lib/types/entry-types.js'; import { defaultNumberPerThread, rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import type { SIWEAuthRequest, SIWEMessage, SIWESocialProof, } from 'lib/types/siwe-types.js'; import { type SubscriptionUpdateRequest, type SubscriptionUpdateResponse, threadSubscriptionValidator, } from 'lib/types/subscription-types.js'; import { rawThreadInfoValidator } from 'lib/types/thread-types.js'; import { createUpdatesResultValidator } from 'lib/types/update-types.js'; import { type PasswordUpdate, loggedOutUserInfoValidator, loggedInUserInfoValidator, oldLoggedInUserInfoValidator, userInfoValidator, } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { identityKeysBlobValidator, signedIdentityKeysBlobValidator, } from 'lib/utils/crypto-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { promiseAll } from 'lib/utils/promises.js'; import { getPublicKeyFromSIWEStatement, isValidSIWEMessage, isValidSIWEStatementWithPublicKey, primaryIdentityPublicKeyRegex, } from 'lib/utils/siwe-utils.js'; import { tShape, tPlatformDetails, tPassword, tEmail, tOldValidUsername, tRegex, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createAccount, processSIWEAccountCreation, } from '../creators/account-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { deleteAccount } from '../deleters/account-deleters.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import { checkAndInvalidateSIWENonceEntry } from '../deleters/siwe-nonce-deleters.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchKnownUserInfos, fetchLoggedInUserInfo, fetchUserIDForEthereumAddress, } from '../fetchers/user-fetchers.js'; import { createNewAnonymousCookie, createNewUserCookie, setNewSession, } from '../session/cookies.js'; +import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; import { accountUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updatePassword, updateUserSettings, updateUserAvatar, } from '../updaters/account-updaters.js'; import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; import { getOlmUtility } from '../utils/olm-utils.js'; import { validateInput, validateOutput } from '../utils/validation-utils.js'; -const subscriptionUpdateRequestInputValidator = tShape({ - threadID: tID, - updatedFields: tShape({ - pushNotifs: t.maybe(t.Boolean), - home: t.maybe(t.Boolean), - }), -}); +const subscriptionUpdateRequestInputValidator = + tShape({ + threadID: tID, + updatedFields: tShape({ + pushNotifs: t.maybe(t.Boolean), + home: t.maybe(t.Boolean), + }), + }); export const subscriptionUpdateResponseValidator: TInterface = tShape({ threadSubscription: threadSubscriptionValidator, }); async function userSubscriptionUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: SubscriptionUpdateRequest = input; - await validateInput(viewer, subscriptionUpdateRequestInputValidator, request); + const request = await validateInput( + viewer, + subscriptionUpdateRequestInputValidator, + input, + ); const threadSubscription = await userSubscriptionUpdater(viewer, request); return validateOutput(viewer, subscriptionUpdateResponseValidator, { threadSubscription, }); } -const accountUpdateInputValidator = tShape({ +const accountUpdateInputValidator = tShape({ updatedFields: tShape({ email: t.maybe(tEmail), password: t.maybe(tPassword), }), currentPassword: tPassword, }); async function passwordUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: PasswordUpdate = input; - await validateInput(viewer, accountUpdateInputValidator, request); + const request = await validateInput( + viewer, + accountUpdateInputValidator, + input, + ); await accountUpdater(viewer, request); } async function sendVerificationEmailResponder(viewer: Viewer): Promise { - await validateInput(viewer, null, null); + if (!viewer.isSocket) { + await verifyClientSupported(viewer, viewer.platformDetails); + } await checkAndSendVerificationEmail(viewer); } -const resetPasswordRequestInputValidator = tShape({ +const resetPasswordRequestInputValidator = tShape({ usernameOrEmail: t.union([tEmail, tOldValidUsername]), }); async function sendPasswordResetEmailResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: ResetPasswordRequest = input; - await validateInput(viewer, resetPasswordRequestInputValidator, request); + const request = await validateInput( + viewer, + resetPasswordRequestInputValidator, + input, + ); await checkAndSendPasswordResetEmail(request); } export const logOutResponseValidator: TInterface = tShape({ currentUserInfo: loggedOutUserInfoValidator, }); async function logOutResponder(viewer: Viewer): Promise { - await validateInput(viewer, null, null); + if (!viewer.isSocket) { + await verifyClientSupported(viewer, viewer.platformDetails); + } if (viewer.loggedIn) { const [anonymousViewerData] = await Promise.all([ createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(anonymousViewerData); } const response = { currentUserInfo: { id: viewer.id, anonymous: true, }, }; return validateOutput(viewer, logOutResponseValidator, response); } -const deleteAccountRequestInputValidator = tShape({ +const deleteAccountRequestInputValidator = tShape({ password: t.maybe(tPassword), }); async function accountDeletionResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: DeleteAccountRequest = input; - await validateInput(viewer, deleteAccountRequestInputValidator, request); + const request = await validateInput( + viewer, + deleteAccountRequestInputValidator, + input, + ); const result = await deleteAccount(viewer, request); invariant(result, 'deleteAccount should return result if handed request'); return validateOutput(viewer, logOutResponseValidator, result); } const deviceTokenUpdateRequestInputValidator = tShape({ deviceType: t.maybe(t.enums.of(['ios', 'android'])), deviceToken: t.String, }); -const registerRequestInputValidator = tShape({ +const registerRequestInputValidator = tShape({ username: t.String, email: t.maybe(tEmail), password: tPassword, calendarQuery: t.maybe(newEntryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), }); export const registerResponseValidator: TInterface = tShape({ id: t.String, rawMessageInfos: t.list(rawMessageInfoValidator), currentUserInfo: t.union([ oldLoggedInUserInfoValidator, loggedInUserInfoValidator, ]), cookieChange: tShape({ threadInfos: t.dict(t.String, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), }); async function accountCreationResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: RegisterRequest = input; - await validateInput(viewer, registerRequestInputValidator, request); + const request = await validateInput( + viewer, + registerRequestInputValidator, + input, + ); const { signedIdentityKeysBlob } = request; if (signedIdentityKeysBlob) { const identityKeys: IdentityKeysBlob = JSON.parse( signedIdentityKeysBlob.payload, ); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } const response = await createAccount(viewer, request); return validateOutput(viewer, registerResponseValidator, response); } type ProcessSuccessfulLoginParams = { +viewer: Viewer, +input: any, +userID: string, +calendarQuery: ?CalendarQuery, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, }; async function processSuccessfulLogin( params: ProcessSuccessfulLoginParams, ): Promise { const { viewer, input, userID, calendarQuery, socialProof, signedIdentityKeysBlob, } = params; const request: LogInRequest = input; const newServerTime = Date.now(); const deviceToken = request.deviceTokenUpdateRequest ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [userViewerData, notAcknowledgedPolicies] = await Promise.all([ createNewUserCookie(userID, { platformDetails: request.platformDetails, deviceToken, socialProof, signedIdentityKeysBlob, }), fetchNotAcknowledgedPolicies(userID, baseLegalPolicies), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(userViewerData); if ( notAcknowledgedPolicies.length && hasMinCodeVersion(viewer.platformDetails, 181) ) { const currentUserInfo = await fetchLoggedInUserInfo(viewer); return { notAcknowledgedPolicies, currentUserInfo: currentUserInfo, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], rawEntryInfos: [], serverTime: 0, cookieChange: { threadInfos: {}, userInfos: [], }, }; } if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } const threadCursors = {}; for (const watchedThreadID of request.watchedIDs) { threadCursors[watchedThreadID] = null; } const messageSelectionCriteria = { threadCursors, joinedThreads: true }; const [ threadsResult, messagesResult, entriesResult, userInfos, currentUserInfo, ] = await Promise.all([ fetchThreadInfos(viewer), fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread), calendarQuery ? fetchEntryInfos(viewer, [calendarQuery]) : undefined, fetchKnownUserInfos(viewer), fetchLoggedInUserInfo(viewer), ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; const response: LogInResponse = { currentUserInfo, rawMessageInfos: messagesResult.rawMessageInfos, truncationStatuses: messagesResult.truncationStatuses, serverTime: newServerTime, userInfos: values(userInfos), cookieChange: { threadInfos: threadsResult.threadInfos, userInfos: [], }, }; if (rawEntryInfos) { return { ...response, rawEntryInfos, }; } return response; } -const logInRequestInputValidator = tShape({ +const logInRequestInputValidator = tShape({ username: t.maybe(t.String), usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, source: t.maybe(t.enums.of(values(logInActionSources))), // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), }); export const logInResponseValidator: TInterface = tShape({ currentUserInfo: t.union([ loggedInUserInfoValidator, oldLoggedInUserInfoValidator, ]), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: t.list(userInfoValidator), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), serverTime: t.Number, cookieChange: tShape({ threadInfos: t.dict(tID, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), }); async function logInResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput(viewer, logInRequestInputValidator, input); - const request: LogInRequest = input; + const request = await validateInput( + viewer, + logInRequestInputValidator, + input, + ); let identityKeys: ?IdentityKeysBlob; const { signedIdentityKeysBlob } = request; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } const calendarQuery = request.calendarQuery ? normalizeCalendarQuery(request.calendarQuery) : null; const promises = {}; if (calendarQuery) { promises.verifyCalendarQueryThreadIDs = verifyCalendarQueryThreadIDs(calendarQuery); } const username = request.username ?? request.usernameOrEmail; if (!username) { if (hasMinCodeVersion(viewer.platformDetails, 150)) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userQuery = SQL` SELECT id, hash, username FROM users WHERE LCASE(username) = LCASE(${username}) `; promises.userQuery = dbQuery(userQuery); const { userQuery: [userResult], } = await promiseAll(promises); if (userResult.length === 0) { if (hasMinCodeVersion(viewer.platformDetails, 150)) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userRow = userResult[0]; if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { throw new ServerError('invalid_credentials'); } const id = userRow.id.toString(); const response = await processSuccessfulLogin({ viewer, input, userID: id, calendarQuery, signedIdentityKeysBlob, }); return validateOutput(viewer, logInResponseValidator, response); } -const siweAuthRequestInputValidator = tShape({ +const siweAuthRequestInputValidator = tShape({ signature: t.String, message: t.String, calendarQuery: entryQueryInputValidator, deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, watchedIDs: t.list(tID), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), }); async function siweAuthResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput(viewer, siweAuthRequestInputValidator, input); - const request: SIWEAuthRequest = input; + const request = await validateInput( + viewer, + siweAuthRequestInputValidator, + input, + ); + const { message, signature, deviceTokenUpdateRequest, platformDetails, signedIdentityKeysBlob, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Ensure that `message` is a well formed Comm SIWE Auth message. const siweMessage: SIWEMessage = new SiweMessage(message); if (!isValidSIWEMessage(siweMessage)) { throw new ServerError('invalid_parameters'); } // 2. Ensure that the `nonce` exists in the `siwe_nonces` table // AND hasn't expired. If those conditions are met, delete the entry to // ensure that the same `nonce` can't be re-used in a future request. const wasNonceCheckedAndInvalidated = await checkAndInvalidateSIWENonceEntry( siweMessage.nonce, ); if (!wasNonceCheckedAndInvalidated) { throw new ServerError('invalid_parameters'); } // 3. Validate SIWEMessage signature and handle possible errors. try { await siweMessage.validate(signature); } catch (error) { if (error === ErrorTypes.EXPIRED_MESSAGE) { // Thrown when the `expirationTime` is present and in the past. throw new ServerError('expired_message'); } else if (error === ErrorTypes.INVALID_SIGNATURE) { // Thrown when the `validate()` function can't verify the message. throw new ServerError('invalid_signature'); } else if (error === ErrorTypes.MALFORMED_SESSION) { // Thrown when some required field is missing. throw new ServerError('malformed_session'); } else { throw new ServerError('unknown_error'); } } // 4. Pull `primaryIdentityPublicKey` out from SIWEMessage `statement`. // We expect it to be included for BOTH native and web clients. const { statement } = siweMessage; const primaryIdentityPublicKey = statement && isValidSIWEStatementWithPublicKey(statement) ? getPublicKeyFromSIWEStatement(statement) : null; if (!primaryIdentityPublicKey) { throw new ServerError('invalid_siwe_statement_public_key'); } // 5. Verify `signedIdentityKeysBlob.payload` with included `signature` // if `signedIdentityKeysBlob` was included in the `SIWEAuthRequest`. let identityKeys: ?IdentityKeysBlob; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } // 6. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE // statement `primaryIdentityPublicKey` if `identityKeys` exists. if ( identityKeys && identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey ) { throw new ServerError('primary_public_key_mismatch'); } // 7. Construct `SIWESocialProof` object with the stringified // SIWEMessage and the corresponding signature. const socialProof: SIWESocialProof = { siweMessage: siweMessage.toMessage(), siweMessageSignature: signature, }; // 8. Create account with call to `processSIWEAccountCreation(...)` // if address does not correspond to an existing user. let userID = await fetchUserIDForEthereumAddress(siweMessage.address); if (!userID) { const siweAccountCreationRequest = { address: siweMessage.address, calendarQuery, deviceTokenUpdateRequest, platformDetails, socialProof, }; userID = await processSIWEAccountCreation( viewer, siweAccountCreationRequest, ); } // 9. Complete login with call to `processSuccessfulLogin(...)`. const response = await processSuccessfulLogin({ viewer, input, userID, calendarQuery, socialProof, signedIdentityKeysBlob, }); return validateOutput(viewer, logInResponseValidator, response); } -const updatePasswordRequestInputValidator = tShape({ +const updatePasswordRequestInputValidator = tShape({ code: t.String, password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, }); async function oldPasswordUpdateResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - await validateInput(viewer, updatePasswordRequestInputValidator, input); - const request: UpdatePasswordRequest = input; + const request = await validateInput( + viewer, + updatePasswordRequestInputValidator, + input, + ); + if (request.calendarQuery) { request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); } const response = await updatePassword(viewer, request); return validateOutput(viewer, logInResponseValidator, response); } -const updateUserSettingsInputValidator = tShape({ +const updateUserSettingsInputValidator = tShape({ name: t.irreducible( userSettingsTypes.DEFAULT_NOTIFICATIONS, x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, ), data: t.enums.of(notificationTypeValues), }); async function updateUserSettingsResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: UpdateUserSettingsRequest = input; - await validateInput(viewer, updateUserSettingsInputValidator, request); + const request = await validateInput( + viewer, + updateUserSettingsInputValidator, + input, + ); await updateUserSettings(viewer, request); } -const policyAcknowledgmentRequestInputValidator = tShape({ - policy: t.maybe(t.enums.of(policies)), -}); +const policyAcknowledgmentRequestInputValidator = + tShape({ + policy: t.maybe(t.enums.of(policies)), + }); async function policyAcknowledgmentResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: PolicyAcknowledgmentRequest = input; - await validateInput( + const request = await validateInput( viewer, policyAcknowledgmentRequestInputValidator, - request, + input, ); await viewerAcknowledgmentUpdater(viewer, request.policy); } export const updateUserAvatarResponseValidator: TInterface = tShape({ updates: createUpdatesResultValidator, }); const updateUserAvatarResponderValidator = t.union([ t.maybe(clientAvatarValidator), updateUserAvatarResponseValidator, ]); async function updateUserAvatarResponder( viewer: Viewer, - input: any, + input: mixed, ): Promise { - const request: UpdateUserAvatarRequest = input; - await validateInput(viewer, updateUserAvatarRequestValidator, request); + const request = await validateInput( + viewer, + updateUserAvatarRequestValidator, + input, + ); const result = await updateUserAvatar(viewer, request); return validateOutput(viewer, updateUserAvatarResponderValidator, result); } export { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, }; diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index 6c76c1a33..b48b3a388 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,329 +1,343 @@ // @flow import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import type { TType, TInterface } from 'tcomb'; import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import { isWebPlatform } from 'lib/types/device-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, tID, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; +const convertToNewIDSchema = false; +const keyserverPrefixID = '256'; + async function validateInput( viewer: Viewer, - inputValidator: ?TType, - input: T, -) { + inputValidator: TType, + input: mixed, +): Promise { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } - checkInputValidator(inputValidator, input); -} + const convertedInput = checkInputValidator(inputValidator, input); -const convertToNewIDSchema = false; -const keyserverPrefixID = '256'; + if ( + hasMinCodeVersion(viewer.platformDetails, 1000) && + !isWebPlatform(viewer.platformDetails?.platform) && + convertToNewIDSchema + ) { + return convertClientIDsToServerIDs( + keyserverPrefixID, + inputValidator, + convertedInput, + ); + } + + return convertedInput; +} function validateOutput( viewer: Viewer, outputValidator: TType, data: T, ): T { if (!outputValidator.is(data)) { console.trace( 'Output validation failed, validator is:', outputValidator.displayName, ); return data; } if ( hasMinCodeVersion(viewer.platformDetails, 1000) && !isWebPlatform(viewer.platformDetails?.platform) && convertToNewIDSchema ) { return convertServerIDsToClientIDs( keyserverPrefixID, outputValidator, data, ); } return data; } function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = id => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } return `${serverPrefixID}|${id}`; }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = id => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } throw new ServerError('invalid_client_id_prefix'); }; return convertObject(outputValidator, data, [tID], conversionFunction); } -function checkInputValidator(inputValidator: ?TType, input: T) { - if (!inputValidator || inputValidator.is(input)) { - return; +function checkInputValidator(inputValidator: TType, input: mixed): T { + if (inputValidator.is(input)) { + return assertWithValidator(input, inputValidator); } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } return null; } function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, 181)) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, convertServerIDsToClientIDs, convertClientIDsToServerIDs, convertObject, policiesValidator, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index 6e20017c9..1eb46cfd3 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,199 +1,202 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import { type RawMessageInfo, type MessageTruncationStatuses, type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; import { type RawThreadInfo } from './thread-types.js'; import { type UserInfo, type LoggedOutUserInfo, type LoggedInUserInfo, type OldLoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, }; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type RegisterInfo = { ...LogInExtraInfo, +username: string, +password: string, }; type DeviceTokenUpdateRequest = { +deviceToken: string, }; export type RegisterRequest = { +username: string, + +email?: empty, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, + +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, }; export type RegisterResponse = { id: string, rawMessageInfos: $ReadOnlyArray, currentUserInfo: OldLoggedInUserInfo | LoggedInUserInfo, cookieChange: { threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, }, }; export type RegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type DeleteAccountRequest = { +password: ?string, }; export const logInActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', sqliteOpFailure: 'SQLITE_OP_FAILURE', sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', }); export type LogInActionSource = $Values; export type LogInStartingPayload = { +calendarQuery: CalendarQuery, +logInActionSource?: LogInActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, }; export type LogInInfo = { ...LogInExtraInfo, +username: string, +password: string, +logInActionSource: LogInActionSource, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: LogInActionSource, + +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, }; export type LogInResponse = { +currentUserInfo: LoggedInUserInfo | OldLoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type LogInResult = { +threadInfos: { +[id: string]: RawThreadInfo }, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: number, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const defaultNotificationPayloadValidator: TInterface = tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); diff --git a/lib/types/entry-types.js b/lib/types/entry-types.js index c784932c7..c10a7ea43 100644 --- a/lib/types/entry-types.js +++ b/lib/types/entry-types.js @@ -1,226 +1,230 @@ // @flow import t, { type TInterface } from 'tcomb'; import { type Platform, isWebPlatform } from './device-types.js'; import { type CalendarFilter, defaultCalendarFilters } from './filter-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { ServerCreateUpdatesResponse, ClientCreateUpdatesResponse, } from './update-types.js'; import type { UserInfo, AccountUserInfo } from './user-types.js'; import { fifteenDaysEarlier, fifteenDaysLater, thisMonthDates, } from '../utils/date-utils.js'; import { tID, tShape } from '../utils/validation-utils.js'; export type RawEntryInfo = { id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, text: string, year: number, month: number, // 1-indexed day: number, // 1-indexed creationTime: number, // millisecond timestamp creatorID: string, deleted: boolean, }; export const rawEntryInfoValidator: TInterface = tShape({ id: t.maybe(tID), localID: t.maybe(t.String), threadID: tID, text: t.String, year: t.Number, month: t.Number, day: t.Number, creationTime: t.Number, creatorID: t.String, deleted: t.Boolean, }); export type EntryInfo = { id?: string, // null if local copy without ID yet localID?: string, // for optimistic creations threadID: string, text: string, year: number, month: number, // 1-indexed day: number, // 1-indexed creationTime: number, // millisecond timestamp creator: ?UserInfo, deleted: boolean, }; export type EntryStore = { +entryInfos: { +[id: string]: RawEntryInfo }, +daysToEntries: { +[day: string]: string[] }, +lastUserInteractionCalendar: number, }; export type CalendarQuery = { +startDate: string, +endDate: string, +filters: $ReadOnlyArray, }; export const defaultCalendarQuery = ( platform: ?Platform, timeZone?: ?string, ): CalendarQuery => { if (isWebPlatform(platform)) { return { ...thisMonthDates(timeZone), filters: defaultCalendarFilters, }; } else { return { startDate: fifteenDaysEarlier(timeZone).valueOf(), endDate: fifteenDaysLater(timeZone).valueOf(), filters: defaultCalendarFilters, }; } }; export type SaveEntryInfo = { +entryID: string, +text: string, +prevText: string, +timestamp: number, +calendarQuery: CalendarQuery, }; export type SaveEntryRequest = { +entryID: string, + +sessionID?: empty, +text: string, +prevText: string, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type SaveEntryResponse = { +entryID: string, +newMessageInfos: $ReadOnlyArray, +updatesResult: ServerCreateUpdatesResponse, }; export type SaveEntryResult = { +entryID: string, +newMessageInfos: $ReadOnlyArray, +updatesResult: ClientCreateUpdatesResponse, }; export type SaveEntryPayload = { ...SaveEntryResult, +threadID: string, }; export type CreateEntryInfo = { +text: string, +timestamp: number, +date: string, +threadID: string, +localID: string, +calendarQuery: CalendarQuery, }; export type CreateEntryRequest = { +text: string, + +sessionID?: empty, +timestamp: number, +date: string, +threadID: string, +localID?: string, +calendarQuery?: CalendarQuery, }; export type CreateEntryPayload = { ...SaveEntryPayload, +localID: string, }; export type DeleteEntryInfo = { +entryID: string, +prevText: string, +calendarQuery: CalendarQuery, }; export type DeleteEntryRequest = { +entryID: string, + +sessionID?: empty, +prevText: string, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type RestoreEntryInfo = { +entryID: string, +calendarQuery: CalendarQuery, }; export type RestoreEntryRequest = { +entryID: string, + +sessionID?: empty, +timestamp: number, +calendarQuery?: CalendarQuery, }; export type DeleteEntryResponse = { +newMessageInfos: $ReadOnlyArray, +threadID: string, +updatesResult: ServerCreateUpdatesResponse, }; export type DeleteEntryResult = { +newMessageInfos: $ReadOnlyArray, +threadID: string, +updatesResult: ClientCreateUpdatesResponse, }; export type RestoreEntryResponse = { +newMessageInfos: $ReadOnlyArray, +updatesResult: ServerCreateUpdatesResponse, }; export type RestoreEntryResult = { +newMessageInfos: $ReadOnlyArray, +updatesResult: ClientCreateUpdatesResponse, }; export type RestoreEntryPayload = { ...RestoreEntryResult, +threadID: string, }; export type FetchEntryInfosBase = { +rawEntryInfos: $ReadOnlyArray, }; export type FetchEntryInfosResponse = { ...FetchEntryInfosBase, +userInfos: { [id: string]: AccountUserInfo }, }; export type FetchEntryInfosResult = FetchEntryInfosBase; export type DeltaEntryInfosResponse = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, }; export type DeltaEntryInfosResult = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export type CalendarResult = { +rawEntryInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type CalendarQueryUpdateStartingPayload = { +calendarQuery?: CalendarQuery, }; export type CalendarQueryUpdateResult = { +rawEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +calendarQuery: CalendarQuery, +calendarQueryAlreadyUpdated: boolean, }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index b2b70c757..9a02eeccf 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,573 +1,574 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { type AvatarDBContent, type ClientAvatar, type UpdateUserAvatarRequest, clientAvatarValidator, } from './avatar-types.js'; import type { Shape } from './core.js'; import type { CalendarQuery, RawEntryInfo } from './entry-types.js'; import type { Media } from './media-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from './message-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import type { ServerUpdateInfo, ClientUpdateInfo } from './update-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import type { ThreadEntity } from '../utils/entity-text.js'; import { values } from '../utils/objects.js'; import { tNumEnum, tBool, tID, tShape } from '../utils/validation-utils.js'; export const threadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) // has parent, not top-level (appears under parent in inbox), and visible to // all members of parent SIDEBAR: 5, // canonical thread for each pair of users. represents the friendship PERSONAL: 6, // canonical thread for each single user PRIVATE: 7, // local "thick" thread (outside of community). no parent, can only have // sidebar children. currently a proxy for COMMUNITY_SECRET_SUBTHREAD until we // launch actual E2E LOCAL: 4, // aka "org". no parent, top-level, has admin COMMUNITY_ROOT: 8, // like COMMUNITY_ROOT, but members aren't voiced COMMUNITY_ANNOUNCEMENT_ROOT: 9, // an open subthread. has parent, top-level (not sidebar), and visible to all // members of parent. root ancestor is a COMMUNITY_ROOT COMMUNITY_OPEN_SUBTHREAD: 3, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD: 10, // a secret subthread. optional parent, top-level (not sidebar), visible only // to its members. root ancestor is a COMMUNITY_ROOT COMMUNITY_SECRET_SUBTHREAD: 4, // like COMMUNITY_SECRET_SUBTHREAD, but members aren't voiced COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD: 11, // like COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, but you can't leave GENESIS: 12, }); export type ThreadType = $Values; export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7 || threadType === 8 || threadType === 9 || threadType === 10 || threadType === 11 || threadType === 12, 'number is not ThreadType enum', ); return threadType; } const threadTypeValidator = tNumEnum(values(threadTypes)); export const communityThreadTypes: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.GENESIS, ]); export const communitySubthreads: $ReadOnlyArray = Object.freeze([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]); export function threadTypeIsCommunityRoot(threadType: ThreadType): boolean { return communityThreadTypes.includes(threadType); } export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', MEMBERSHIP_DEPRECATED: 'membership', VISIBLE: 'visible', VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD_NAME: 'edit_thread', EDIT_THREAD_DESCRIPTION: 'edit_thread_description', EDIT_THREAD_COLOR: 'edit_thread_color', DELETE_THREAD: 'delete_thread', CREATE_SUBCHANNELS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLE: 'change_role', LEAVE_THREAD: 'leave_thread', REACT_TO_MESSAGE: 'react_to_message', EDIT_MESSAGE: 'edit_message', EDIT_THREAD_AVATAR: 'edit_thread_avatar', MANAGE_PINS: 'manage_pins', }); export type ThreadPermission = $Values; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || ourThreadPermissions === 'membership' || ourThreadPermissions === 'visible' || ourThreadPermissions === 'voiced' || ourThreadPermissions === 'edit_entries' || ourThreadPermissions === 'edit_thread' || ourThreadPermissions === 'edit_thread_description' || ourThreadPermissions === 'edit_thread_color' || ourThreadPermissions === 'delete_thread' || ourThreadPermissions === 'create_subthreads' || ourThreadPermissions === 'create_sidebars' || ourThreadPermissions === 'join_thread' || ourThreadPermissions === 'edit_permissions' || ourThreadPermissions === 'add_members' || ourThreadPermissions === 'remove_members' || ourThreadPermissions === 'change_role' || ourThreadPermissions === 'leave_thread' || ourThreadPermissions === 'react_to_message' || ourThreadPermissions === 'edit_message' || ourThreadPermissions === 'edit_thread_avatar' || ourThreadPermissions === 'manage_pins', 'string is not threadPermissions enum', ); return ourThreadPermissions; } const threadPermissionValidator = t.enums.of(values(threadPermissions)); export const threadPermissionPropagationPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', }); export type ThreadPermissionPropagationPrefix = $Values< typeof threadPermissionPropagationPrefixes, >; export const threadPermissionFilterPrefixes = Object.freeze({ // includes only SIDEBAR, COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN: 'open_', // excludes only SIDEBAR TOP_LEVEL: 'toplevel_', // includes only COMMUNITY_OPEN_SUBTHREAD, // COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD OPEN_TOP_LEVEL: 'opentoplevel_', }); export type ThreadPermissionFilterPrefix = $Values< typeof threadPermissionFilterPrefixes, >; export type ThreadPermissionInfo = | { +value: true, +source: string } | { +value: false, +source: null }; const threadPermissionInfoValidator = t.union([ tShape({ value: tBool(true), source: t.String }), tShape({ value: tBool(false), source: t.Nil }), ]); export type ThreadPermissionsBlob = { +[permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { +[permission: string]: boolean }; const threadRolePermissionsBlobValidator = t.dict(t.String, t.Boolean); export type ThreadPermissionsInfo = { +[permission: ThreadPermission]: ThreadPermissionInfo, }; const threadPermissionsInfoValidator = t.dict( threadPermissionValidator, threadPermissionInfoValidator, ); export type MemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +isSender: boolean, }; const memberInfoValidator = tShape({ id: t.String, role: t.maybe(tID), permissions: threadPermissionsInfoValidator, isSender: t.Boolean, }); export type RelativeMemberInfo = { ...MemberInfo, +username: ?string, +isViewer: boolean, }; export type RoleInfo = { +id: string, +name: string, +permissions: ThreadRolePermissionsBlob, +isDefault: boolean, }; const roleInfoValidator = tShape({ id: tID, name: t.String, permissions: threadRolePermissionsBlobValidator, isDefault: t.Boolean, }); export type ThreadCurrentUserInfo = { +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, }; const threadCurrentUserInfoValidator = tShape({ role: t.maybe(tID), permissions: threadPermissionsInfoValidator, subscription: threadSubscriptionValidator, unread: t.maybe(t.Boolean), }); export type RawThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export const rawThreadInfoValidator: TInterface = tShape({ id: tID, type: threadTypeValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.String, creationTime: t.Number, parentThreadID: t.maybe(tID), containingThreadID: t.maybe(tID), community: t.maybe(tID), members: t.list(memberInfoValidator), roles: t.dict(tID, roleInfoValidator), currentUser: threadCurrentUserInfoValidator, sourceMessageID: t.maybe(tID), repliesCount: t.Number, pinnedCount: t.maybe(t.Number), }); export type ThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +uiName: string | ThreadEntity, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ResolvedThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +uiName: string, +avatar?: ?ClientAvatar, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +currentUser: ThreadCurrentUserInfo, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ServerMemberInfo = { +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, +isSender: boolean, }; export type ServerThreadInfo = { +id: string, +type: ThreadType, +name: ?string, +avatar?: AvatarDBContent, +description: ?string, +color: string, // hex, without "#" or "0x" +creationTime: number, // millisecond timestamp +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +depth: number, +members: $ReadOnlyArray, +roles: { +[id: string]: RoleInfo }, +sourceMessageID?: string, +repliesCount: number, +pinnedCount: number, }; export type ThreadStore = { +threadInfos: { +[id: string]: RawThreadInfo }, }; export type RemoveThreadOperation = { +type: 'remove', +payload: { +ids: $ReadOnlyArray }, }; export type RemoveAllThreadsOperation = { +type: 'remove_all', }; export type ReplaceThreadOperation = { +type: 'replace', +payload: { +id: string, +threadInfo: RawThreadInfo }, }; export type ThreadStoreOperation = | RemoveThreadOperation | RemoveAllThreadsOperation | ReplaceThreadOperation; export type ClientDBThreadInfo = { +id: string, +type: number, +name: ?string, +avatar?: ?string, +description: ?string, +color: string, +creationTime: string, +parentThreadID: ?string, +containingThreadID: ?string, +community: ?string, +members: string, +roles: string, +currentUser: string, +sourceMessageID?: string, +repliesCount: number, +pinnedCount?: number, }; export type ClientDBReplaceThreadOperation = { +type: 'replace', +payload: ClientDBThreadInfo, }; export type ClientDBThreadStoreOperation = | RemoveThreadOperation | RemoveAllThreadsOperation | ClientDBReplaceThreadOperation; export type ThreadDeletionRequest = { +threadID: string, +accountPassword: ?string, }; export type RemoveMembersRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, }; export type RoleChangeRequest = { +threadID: string, +memberIDs: $ReadOnlyArray, +role: string, }; export type ChangeThreadSettingsResult = { +threadInfo?: RawThreadInfo, +threadInfos?: { +[id: string]: RawThreadInfo }, +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type ChangeThreadSettingsPayload = { +threadID: string, +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, }; export type LeaveThreadRequest = { +threadID: string, }; export type LeaveThreadResult = { +threadInfos?: { +[id: string]: RawThreadInfo }, +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type LeaveThreadPayload = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, }; export type ThreadChanges = Shape<{ +type: ThreadType, +name: string, +description: string, +color: string, +parentThreadID: ?string, +newMemberIDs: $ReadOnlyArray, +avatar: UpdateUserAvatarRequest, }>; export type UpdateThreadRequest = { +threadID: string, +changes: ThreadChanges, + +accountPassword?: empty, }; export type BaseNewThreadRequest = { +id?: ?string, +name?: ?string, +description?: ?string, +color?: ?string, +parentThreadID?: ?string, +initialMemberIDs?: ?$ReadOnlyArray, +ghostMemberIDs?: ?$ReadOnlyArray, }; type NewThreadRequest = | { +type: 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12, ...BaseNewThreadRequest, } | { +type: 5, +sourceMessageID: string, ...BaseNewThreadRequest, }; export type ClientNewThreadRequest = { ...NewThreadRequest, +calendarQuery: CalendarQuery, }; export type ServerNewThreadRequest = { ...NewThreadRequest, +calendarQuery?: ?CalendarQuery, }; export type NewThreadResponse = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +newThreadInfo?: RawThreadInfo, +userInfos: UserInfos, +newThreadID?: string, }; export type NewThreadResult = { +updatesResult: { +newUpdates: $ReadOnlyArray, }, +newMessageInfos: $ReadOnlyArray, +userInfos: UserInfos, +newThreadID: string, }; export type ServerThreadJoinRequest = { +threadID: string, +calendarQuery?: ?CalendarQuery, +inviteLinkSecret?: string, }; export type ClientThreadJoinRequest = { +threadID: string, +calendarQuery: CalendarQuery, +inviteLinkSecret?: string, }; export type ThreadJoinResult = { threadInfos?: { +[id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, rawMessageInfos: $ReadOnlyArray, truncationStatuses: MessageTruncationStatuses, userInfos: UserInfos, rawEntryInfos?: ?$ReadOnlyArray, }; export type ThreadJoinPayload = { +updatesResult: { newUpdates: $ReadOnlyArray, }, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, }; export type ThreadFetchMediaResult = { +media: $ReadOnlyArray, }; export type ThreadFetchMediaRequest = { +threadID: string, +limit: number, +offset: number, }; export type SidebarInfo = { +threadInfo: ThreadInfo, +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, }; export type ToggleMessagePinRequest = { +messageID: string, +action: 'pin' | 'unpin', }; export type ToggleMessagePinResult = { +newMessageInfos: $ReadOnlyArray, +threadID: string, }; // We can show a max of 3 sidebars inline underneath their parent in the chat // tab. If there are more, we show a button that opens a modal to see the rest export const maxReadSidebars = 3; // We can show a max of 5 sidebars inline underneath their parent // in the chat tab if every one of the displayed sidebars is unread export const maxUnreadSidebars = 5; export type ThreadStoreThreadInfos = { +[id: string]: RawThreadInfo };