diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -70,6 +70,7 @@ editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, + deleteMessageResponder, sendMultimediaMessageRequestInputValidator, sendReactionMessageRequestInputValidator, editMessageRequestInputValidator, @@ -77,6 +78,7 @@ fetchMessageInfosRequestInputValidator, fetchPinnedMessagesResponderInputValidator, searchMessagesResponderInputValidator, + deleteMessageRequestValidator, } from './responders/message-responders.js'; import { getInitialReduxStateResponder, @@ -521,6 +523,11 @@ inputValidator: deleteFarcasterChannelTagInputValidator, policies: baseLegalPolicies, }, + delete_message: { + responder: deleteMessageResponder, + inputValidator: deleteMessageRequestValidator, + policies: baseLegalPolicies, + }, }; function createJSONResponders(obj: { +[Endpoint]: EndpointData }): { diff --git a/keyserver/src/fetchers/thread-permission-fetchers.js b/keyserver/src/fetchers/thread-permission-fetchers.js --- a/keyserver/src/fetchers/thread-permission-fetchers.js +++ b/keyserver/src/fetchers/thread-permission-fetchers.js @@ -68,6 +68,20 @@ return checkThread(viewer, threadID, [{ check: 'permission', permission }]); } +async function checkThreadHasAnyPermission( + viewer: Viewer, + threadID: string, + permissions: $ReadOnlyArray, +): Promise { + const checks = permissions.map(permission => ({ + check: 'permission', + permission, + })); + + const threadsWithPermissions = await checkThreadsOr(viewer, threadID, checks); + return threadsWithPermissions.has(threadID); +} + function viewerIsMember(viewer: Viewer, threadID: string): Promise { return checkThread(viewer, threadID, [{ check: 'is_member' }]); } @@ -523,4 +537,5 @@ checkIfThreadIsBlocked, validateCandidateMembers, viewerHasPositiveRole, + checkThreadHasAnyPermission, }; diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js --- a/keyserver/src/responders/message-responders.js +++ b/keyserver/src/responders/message-responders.js @@ -25,7 +25,11 @@ type FetchPinnedMessagesResult, type SearchMessagesResponse, type SearchMessagesKeyserverRequest, + type DeleteMessageRequest, + type DeleteMessageResponse, + isComposableMessageType, } from 'lib/types/message-types.js'; +import type { DeleteMessageData } from 'lib/types/messages/delete.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'; @@ -49,7 +53,10 @@ searchMessagesInSingleChat, } from '../fetchers/message-fetchers.js'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; -import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; +import { + checkThreadHasAnyPermission, + checkThreadPermission, +} from '../fetchers/thread-permission-fetchers.js'; import { fetchUnassignedImages, fetchUnassignedMediaFromMediaMessageContent, @@ -339,8 +346,11 @@ 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 + // When we try to edit a message from which a sidebar was created, the + // client should send the ID of the message from the parent thread. + // We're checking here if the ID from a sidebar was sent instead - this + // is a mistake because editing it would result in a difference between + // how the message is displayed in the parent thread and in the sidebar. throw new ServerError('invalid_parameters'); } @@ -415,6 +425,88 @@ ); } +export const deleteMessageRequestValidator: TInterface = + tShape({ + targetMessageID: tID, + }); + +async function deleteMessageResponder( + viewer: Viewer, + request: DeleteMessageRequest, +): Promise { + const { targetMessageID } = request; + + const targetMessageInfo = await fetchMessageInfoByID(viewer, targetMessageID); + if (!targetMessageInfo || !targetMessageInfo.id) { + throw new ServerError('invalid_parameters'); + } + + if (!isComposableMessageType(targetMessageInfo.type)) { + throw new ServerError('invalid_parameters'); + } + + const { threadID } = targetMessageInfo; + + const permissionsToCheck = + targetMessageInfo.creatorID === viewer.id + ? [ + threadPermissions.DELETE_ALL_MESSAGES, + threadPermissions.DELETE_OWN_MESSAGES, + ] + : [threadPermissions.DELETE_ALL_MESSAGES]; + + const [serverThreadInfos, hasPermission, rawSidebarThreadInfos] = + await Promise.all([ + fetchServerThreadInfos({ threadID }), + checkThreadHasAnyPermission(viewer, threadID, permissionsToCheck), + fetchServerThreadInfos({ + parentThreadID: threadID, + sourceMessageID: targetMessageID, + }), + ]); + + const targetMessageThreadInfo = serverThreadInfos.threadInfos[threadID]; + if (targetMessageThreadInfo.sourceMessageID === targetMessageID) { + // When we try to delete a message from which a sidebar was created, the + // client should send the ID of the message from the parent thread. + // We're checking here if the ID from a sidebar was sent instead - this + // is a mistake because deleting it would result in a difference between + // how the message is displayed in the parent thread and in the sidebar. + throw new ServerError('invalid_parameters'); + } + + if (!hasPermission) { + throw new ServerError('invalid_parameters'); + } + + const time = Date.now(); + const messagesData: Array = []; + messagesData.push({ + type: messageTypes.DELETE_MESSAGE, + threadID, + creatorID: viewer.id, + time, + targetMessageID, + }); + + const sidebarThreadValues = values(rawSidebarThreadInfos.threadInfos); + for (const sidebarThreadValue of sidebarThreadValues) { + if (sidebarThreadValue && sidebarThreadValue.id) { + messagesData.push({ + type: messageTypes.DELETE_MESSAGE, + threadID: sidebarThreadValue.id, + creatorID: viewer.id, + time, + targetMessageID, + }); + } + } + + const newMessageInfos = await createMessages(viewer, messagesData); + + return { newMessageInfos }; +} + export { textMessageCreationResponder, messageFetchResponder, @@ -423,4 +515,5 @@ editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, + deleteMessageResponder, }; diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -119,6 +119,7 @@ CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG: 'create_or_update_farcaster_channel_tag', DELETE_FARCASTER_CHANNEL_TAG: 'delete_farcaster_channel_tag', + DELETE_MESSAGE: 'delete_message', }); type HTTPPreferredEndpoint = $Values; diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -675,6 +675,14 @@ +text: string, }; +export type DeleteMessageRequest = { + +targetMessageID: string, +}; + +export type DeleteMessageResponse = { + +newMessageInfos: $ReadOnlyArray, +}; + // Used for the message info included in log-in type actions export type GenericMessagesResult = { +messageInfos: RawMessageInfo[], diff --git a/lib/types/validators/endpoint-validators.js b/lib/types/validators/endpoint-validators.js --- a/lib/types/validators/endpoint-validators.js +++ b/lib/types/validators/endpoint-validators.js @@ -29,6 +29,7 @@ sendEditMessageResponseValidator, sendMessageResponseValidator, searchMessagesResponseValidator, + deleteMessageResponseValidator, } from './message-validators.js'; import { initialReduxStateValidator } from './redux-state-validators.js'; import { relationshipErrorsValidator } from './relationship-validators.js'; @@ -190,6 +191,9 @@ delete_farcaster_channel_tag: { validator: deleteFarcasterChannelTagResponseValidator, }, + delete_message: { + validator: deleteMessageResponseValidator, + }, }); export const endpointValidators = Object.freeze({ diff --git a/lib/types/validators/message-validators.js b/lib/types/validators/message-validators.js --- a/lib/types/validators/message-validators.js +++ b/lib/types/validators/message-validators.js @@ -9,6 +9,7 @@ type SendEditMessageResponse, type FetchPinnedMessagesResult, type SearchMessagesResponse, + type DeleteMessageResponse, } from '../message-types.js'; import { rawMessageInfoValidator, @@ -41,3 +42,8 @@ messages: t.list(rawMessageInfoValidator), endReached: t.Boolean, }); + +export const deleteMessageResponseValidator: TInterface = + tShape({ + newMessageInfos: t.list(rawMessageInfoValidator), + });