diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js index 640bf20a2..9679c0f0e 100644 --- a/keyserver/src/responders/message-responders.js +++ b/keyserver/src/responders/message-responders.js @@ -1,267 +1,271 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import { createMediaMessageData, trimMessage } from 'lib/shared/message-utils'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils'; import type { Media } from 'lib/types/media-types.js'; import { messageTypes, type SendTextMessageRequest, type SendMultimediaMessageRequest, type SendReactionMessageRequest, type FetchMessageInfosResponse, type FetchMessageInfosRequest, defaultNumberPerThread, type SendMessageResponse, } from 'lib/types/message-types'; import type { ReactionMessageData } from 'lib/types/messages/reaction'; import type { TextMessageData } from 'lib/types/messages/text'; import { threadPermissions } from 'lib/types/thread-types'; import { ServerError } from 'lib/utils/errors'; import { tString, tShape, tMediaMessageMedia, } from 'lib/utils/validation-utils'; import createMessages from '../creators/message-creator'; import { SQL } from '../database/database'; import { fetchMessageInfos, fetchMessageInfoForLocalID, fetchMessageInfoByID, } from '../fetchers/message-fetchers'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers'; import { fetchMedia, fetchMediaFromMediaMessageContent, } from '../fetchers/upload-fetchers'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers'; import type { Viewer } from '../session/viewer'; import { assignMedia, assignMessageContainerToMedia, } from '../updaters/upload-updaters'; import { validateInput } from '../utils/validation-utils'; const sendTextMessageRequestInputValidator = tShape({ threadID: t.String, localID: t.maybe(t.String), text: t.String, }); async function textMessageCreationResponder( viewer: Viewer, input: any, ): Promise { const request: SendTextMessageRequest = input; await validateInput(viewer, sendTextMessageRequestInputValidator, request); const { threadID, localID, text: rawText } = 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'); } const messageData: TextMessageData = { type: messageTypes.TEXT, threadID, creatorID: viewer.id, time: Date.now(), text, }; if (localID) { messageData.localID = localID; } const rawMessageInfos = await createMessages(viewer, [messageData]); return { newMessageInfo: rawMessageInfos[0] }; } const fetchMessageInfosRequestInputValidator = tShape({ cursors: t.dict(t.String, t.maybe(t.String)), numberPerThread: t.maybe(t.Number), }); async function messageFetchResponder( viewer: Viewer, input: any, ): Promise { const request: FetchMessageInfosRequest = input; await validateInput(viewer, fetchMessageInfosRequestInputValidator, request); const response = await fetchMessageInfos( viewer, { threadCursors: request.cursors }, request.numberPerThread ? request.numberPerThread : defaultNumberPerThread, ); return { ...response, userInfos: {} }; } const sendMultimediaMessageRequestInputValidator = t.union([ tShape({ threadID: t.String, localID: t.String, mediaIDs: t.list(t.String), }), tShape({ threadID: t.String, localID: t.String, mediaMessageContents: t.list(tMediaMessageMedia), }), ]); async function multimediaMessageCreationResponder( viewer: Viewer, input: any, ): Promise { const request: SendMultimediaMessageRequest = input; await validateInput( viewer, sendMultimediaMessageRequestInputValidator, request, ); if ( (request.mediaIDs && request.mediaIDs.length === 0) || (request.mediaMessageContents && request.mediaMessageContents.length === 0) ) { throw new ServerError('invalid_parameters'); } const { threadID, localID } = 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 ? fetchMedia(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'); } const messageData = createMediaMessageData({ localID, threadID, creatorID: viewer.id, media, }); 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 assignMedia(viewer, request.mediaIDs, id); } else { await assignMessageContainerToMedia( viewer, request.mediaMessageContents, id, ); } return { newMessageInfo }; } const sendReactionMessageRequestInputValidator = tShape({ threadID: t.String, + localID: t.maybe(t.String), targetMessageID: t.String, reaction: tString('👍'), action: t.enums.of(['add_reaction', 'remove_reaction']), }); async function reactionMessageCreationResponder( viewer: Viewer, input: any, ): Promise { const request: SendReactionMessageRequest = input; await validateInput(viewer, sendReactionMessageRequestInputValidator, input); - const { threadID, targetMessageID, reaction, action } = request; + 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.VOICED), 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'); } - const messageData: ReactionMessageData = { + 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]); return { newMessageInfo: rawMessageInfos[0] }; } export { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, }; diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js index c5004e7d3..a603e5c41 100644 --- a/lib/actions/message-actions.js +++ b/lib/actions/message-actions.js @@ -1,261 +1,262 @@ // @flow import invariant from 'invariant'; import type { FetchMessageInfosPayload, SendMessageResult, SendReactionMessageRequest, SimpleMessagesPayload, } from '../types/message-types'; import type { MediaMessageServerDBContent } from '../types/messages/media.js'; import type { CallServerEndpoint, CallServerEndpointResultInfo, } from '../utils/call-server-endpoint'; const fetchMessagesBeforeCursorActionTypes = Object.freeze({ started: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', success: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', failed: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', }); const fetchMessagesBeforeCursor = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, beforeMessageID: string, ) => Promise) => async ( threadID, beforeMessageID, ) => { const response = await callServerEndpoint('fetch_messages', { cursors: { [threadID]: beforeMessageID, }, }); return { threadID, rawMessageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses[threadID], }; }; const fetchMostRecentMessagesActionTypes = Object.freeze({ started: 'FETCH_MOST_RECENT_MESSAGES_STARTED', success: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', failed: 'FETCH_MOST_RECENT_MESSAGES_FAILED', }); const fetchMostRecentMessages = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, ) => Promise) => async threadID => { const response = await callServerEndpoint('fetch_messages', { cursors: { [threadID]: null, }, }); return { threadID, rawMessageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses[threadID], }; }; const fetchSingleMostRecentMessagesFromThreadsActionTypes = Object.freeze({ started: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', success: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', failed: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', }); const fetchSingleMostRecentMessagesFromThreads = ( callServerEndpoint: CallServerEndpoint, ): (( threadIDs: $ReadOnlyArray, ) => Promise) => async threadIDs => { const cursors = Object.fromEntries( threadIDs.map(threadID => [threadID, null]), ); const response = await callServerEndpoint('fetch_messages', { cursors, numberPerThread: 1, }); return { rawMessageInfos: response.rawMessageInfos, truncationStatuses: response.truncationStatuses, }; }; const sendTextMessageActionTypes = Object.freeze({ started: 'SEND_TEXT_MESSAGE_STARTED', success: 'SEND_TEXT_MESSAGE_SUCCESS', failed: 'SEND_TEXT_MESSAGE_FAILED', }); const sendTextMessage = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, localID: string, text: string, ) => Promise) => async (threadID, localID, text) => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const response = await callServerEndpoint( 'create_text_message', { threadID, localID, text, }, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callServerEndpoint resolves', ); return { id: response.newMessageInfo.id, time: response.newMessageInfo.time, interface: resultInterface, }; }; const createLocalMessageActionType = 'CREATE_LOCAL_MESSAGE'; const sendMultimediaMessageActionTypes = Object.freeze({ started: 'SEND_MULTIMEDIA_MESSAGE_STARTED', success: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', failed: 'SEND_MULTIMEDIA_MESSAGE_FAILED', }); const sendMultimediaMessage = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, localID: string, mediaMessageContents: $ReadOnlyArray, ) => Promise) => async ( threadID, localID, mediaMessageContents, ) => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const response = await callServerEndpoint( 'create_multimedia_message', { threadID, localID, mediaMessageContents, }, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callServerEndpoint resolves', ); return { id: response.newMessageInfo.id, time: response.newMessageInfo.time, interface: resultInterface, }; }; const legacySendMultimediaMessage = ( callServerEndpoint: CallServerEndpoint, ): (( threadID: string, localID: string, mediaIDs: $ReadOnlyArray, ) => Promise) => async (threadID, localID, mediaIDs) => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const response = await callServerEndpoint( 'create_multimedia_message', { threadID, localID, mediaIDs, }, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callServerEndpoint resolves', ); return { id: response.newMessageInfo.id, time: response.newMessageInfo.time, interface: resultInterface, }; }; const sendReactionMessageActionTypes = Object.freeze({ started: 'SEND_REACTION_MESSAGE_STARTED', success: 'SEND_REACTION_MESSAGE_SUCCESS', failed: 'SEND_REACTION_MESSAGE_FAILED', }); const sendReactionMessage = ( callServerEndpoint: CallServerEndpoint, ): (( request: SendReactionMessageRequest, ) => Promise) => async request => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const response = await callServerEndpoint( 'create_reaction_message', { threadID: request.threadID, + localID: request.localID, targetMessageID: request.targetMessageID, reaction: request.reaction, action: request.action, }, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callServerEndpoint resolves', ); return { id: response.newMessageInfo.id, time: response.newMessageInfo.time, interface: resultInterface, }; }; const saveMessagesActionType = 'SAVE_MESSAGES'; const processMessagesActionType = 'PROCESS_MESSAGES'; const messageStorePruneActionType = 'MESSAGE_STORE_PRUNE'; export { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, fetchMostRecentMessages, fetchSingleMostRecentMessagesFromThreadsActionTypes, fetchSingleMostRecentMessagesFromThreads, sendTextMessageActionTypes, sendTextMessage, createLocalMessageActionType, sendMultimediaMessageActionTypes, sendMultimediaMessage, legacySendMultimediaMessage, sendReactionMessageActionTypes, sendReactionMessage, saveMessagesActionType, processMessagesActionType, messageStorePruneActionType, };