From 4c9baaef80aba1c65c6eb28bc55130f0621e91cd Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 11 Apr 2023 17:16:46 -0700 Subject: [PATCH] Filter incoming bodyRanges, also filter before display --- ts/models/conversations.ts | 40 ++--------------- ts/models/messages.ts | 27 +---------- ts/state/selectors/message.ts | 21 ++------- ts/state/selectors/search.ts | 25 ++--------- ts/state/selectors/stories.ts | 18 +------- ts/test-both/processDataMessage_test.ts | 2 +- ts/textsecure/MessageReceiver.ts | 5 ++- ts/textsecure/Types.d.ts | 5 ++- ts/textsecure/processDataMessage.ts | 7 ++- ts/types/BodyRange.ts | 60 +++++++++++++++++++++++++ 10 files changed, 84 insertions(+), 126 deletions(-) diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index acb531935..fe8188bed 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -85,7 +85,7 @@ import { } from '../Crypto'; import * as Bytes from '../Bytes'; import type { DraftBodyRangeMention } from '../types/BodyRange'; -import { BodyRange } from '../types/BodyRange'; +import { BodyRange, hydrateRanges } from '../types/BodyRange'; import { migrateColor } from '../util/migrateColor'; import { isNotNil } from '../util/isNotNil'; import { dropNull } from '../util/dropNull'; @@ -1094,24 +1094,7 @@ export class ConversationModel extends window.Backbone const draft = this.get('draft'); const rawBodyRanges = this.get('draftBodyRanges') || []; - const bodyRanges = rawBodyRanges.map(range => { - // Hydrate user information on mention - if (BodyRange.isMention(range)) { - const conversation = findAndFormatContact(range.mentionUuid); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - } - - if (BodyRange.isFormatting(range)) { - return range; - } - - throw missingCaseError(range); - }); + const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact); if (draft) { return { @@ -3902,24 +3885,7 @@ export class ConversationModel extends window.Backbone } const rawBodyRanges = this.get('lastMessageBodyRanges') || []; - const bodyRanges = rawBodyRanges.map(range => { - // Hydrate user information on mention - if (BodyRange.isMention(range)) { - const conversation = findAndFormatContact(range.mentionUuid); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - } - - if (BodyRange.isFormatting(range)) { - return range; - } - - throw missingCaseError(range); - }); + const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact); const text = stripNewlinesForLeftPane(lastMessageText); const prefix = this.get('lastMessagePrefix'); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 3e9cd4c3d..b119bbe35 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -53,7 +53,7 @@ import * as expirationTimer from '../util/expirationTimer'; import { getUserLanguages } from '../util/userLanguages'; import type { ReactionType } from '../types/Reactions'; -import { isValidUuid, UUID, UUIDKind } from '../types/UUID'; +import { UUID, UUIDKind } from '../types/UUID'; import * as reactionUtil from '../reactions/util'; import * as Stickers from '../types/Stickers'; import * as Errors from '../types/errors'; @@ -1957,30 +1957,7 @@ export class MessageModel extends window.Backbone.Model { id, attachments: quote.attachments.slice(), - bodyRanges: quote.bodyRanges - .map(({ start, length, mentionUuid }) => { - strictAssert( - start != null, - 'Received quote with a bodyRange.start == null' - ); - strictAssert( - length != null, - 'Received quote with a bodyRange.length == null' - ); - if (!isValidUuid(mentionUuid)) { - log.warn( - `copyFromQuotedMessage: invalid mentionUuid ${mentionUuid}` - ); - return undefined; - } - - return { - start, - length, - mentionUuid, - }; - }) - .filter(isNotNil), + bodyRanges: quote.bodyRanges?.slice(), // Just placeholder values for the fields referencedMessageNotFound: false, diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 03e4b118c..7536dda54 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -48,7 +48,7 @@ import type { HydratedBodyRangeMention, HydratedBodyRangesType, } from '../../types/BodyRange'; -import { BodyRange } from '../../types/BodyRange'; +import { BodyRange, hydrateRanges } from '../../types/BodyRange'; import type { AssertProps } from '../../types/Util'; import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import { getMentionsRegex } from '../../types/Message'; @@ -317,22 +317,9 @@ export const processBodyRanges = ( return undefined; } - return bodyRanges - .map(range => { - const { conversationSelector } = options; - - if (BodyRange.isMention(range)) { - const conversation = conversationSelector(range.mentionUuid); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - } - return range; - }) - .sort((a, b) => b.start - a.start); + return hydrateRanges(bodyRanges, options.conversationSelector)?.sort( + (a, b) => b.start - a.start + ); }; export const extractHydratedMentions = ( diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index 890a35f8f..00fef486d 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -28,11 +28,9 @@ import { getConversationSelector, } from './conversations'; -import type { HydratedBodyRangeType } from '../../types/BodyRange'; -import { BodyRange } from '../../types/BodyRange'; +import { hydrateRanges } from '../../types/BodyRange'; import * as log from '../../logging/log'; import { getOwn } from '../../util/getOwn'; -import { missingCaseError } from '../../util/missingCaseError'; export const getSearch = (state: StateType): SearchStateType => state.search; @@ -178,7 +176,6 @@ export const getCachedSelectorForMessageSearchResult = createSelector( searchConversationId?: string, targetedMessageId?: string ) => { - const bodyRanges = message.bodyRanges || []; return { from, to, @@ -187,24 +184,8 @@ export const getCachedSelectorForMessageSearchResult = createSelector( conversationId: message.conversationId, sentAt: message.sent_at, snippet: message.snippet || '', - bodyRanges: bodyRanges.map((range): HydratedBodyRangeType => { - // Hydrate user information on mention - if (BodyRange.isMention(range)) { - const conversation = conversationSelector(range.mentionUuid); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - } - - if (BodyRange.isFormatting(range)) { - return range; - } - - throw missingCaseError(range); - }), + bodyRanges: + hydrateRanges(message.bodyRanges, conversationSelector) || [], body: message.body || '', isSelected: Boolean( diff --git a/ts/state/selectors/stories.ts b/ts/state/selectors/stories.ts index 3561deb47..261aab66b 100644 --- a/ts/state/selectors/stories.ts +++ b/ts/state/selectors/stories.ts @@ -42,7 +42,7 @@ import { reduceStorySendStatus, resolveStorySendStatus, } from '../../util/resolveStorySendStatus'; -import { BodyRange } from '../../types/BodyRange'; +import { BodyRange, hydrateRanges } from '../../types/BodyRange'; export const getStoriesState = (state: StateType): StoriesStateType => state.stories; @@ -302,24 +302,10 @@ export const getStoryReplies = createSelector( ? me : conversationSelector(reply.sourceUuid || reply.source); - const { bodyRanges } = reply; - return { author: getAvatarData(conversation), ...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']), - bodyRanges: bodyRanges?.map(bodyRange => { - if (BodyRange.isMention(bodyRange)) { - const mentionConvo = conversationSelector(bodyRange.mentionUuid); - - return { - ...bodyRange, - conversationID: mentionConvo.id, - replacementText: mentionConvo.title, - }; - } - - return bodyRange; - }), + bodyRanges: hydrateRanges(reply.bodyRanges, conversationSelector), reactionEmoji: reply.storyReaction?.emoji, contactNameColor: contactNameColorSelector( reply.conversationId, diff --git a/ts/test-both/processDataMessage_test.ts b/ts/test-both/processDataMessage_test.ts index 31755b3b0..db3088490 100644 --- a/ts/test-both/processDataMessage_test.ts +++ b/ts/test-both/processDataMessage_test.ts @@ -202,7 +202,7 @@ describe('processDataMessage', () => { thumbnail: PROCESSED_ATTACHMENT, }, ], - bodyRanges: [], + bodyRanges: undefined, type: 0, }); }); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 3544ed2ed..bf545553c 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -125,6 +125,7 @@ import { chunk } from '../util/iterables'; import { isOlderThan } from '../util/timestamp'; import { inspectUnknownFieldTags } from '../util/inspectProtobufs'; import { incrementMessageCounter } from '../util/incrementMessageCounter'; +import { filterAndClean } from '../types/BodyRange'; const GROUPV1_ID_LENGTH = 16; const GROUPV2_ID_LENGTH = 32; @@ -2121,8 +2122,8 @@ export default class MessageReceiver const message: ProcessedDataMessage = { attachments, - // We need to remove all of the extra stuff on these objects so serialize properly - bodyRanges: msg.bodyRanges?.map(item => ({ ...item })), + + bodyRanges: filterAndClean(msg.bodyRanges), preview, canReplyToStory: Boolean(msg.allowsReplies), expireTimer: DurationInSeconds.DAY, diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 879599b13..66d9a791d 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -9,6 +9,7 @@ import type { GiftBadgeStates } from '../components/conversation/Message'; import type { MIMEType } from '../types/MIME'; import type { DurationInSeconds } from '../util/durations'; import type { AnyPaymentEvent } from '../types/Payment'; +import type { RawBodyRange } from '../types/BodyRange'; export { IdentityKeyType, @@ -150,7 +151,7 @@ export type ProcessedQuote = { authorUuid?: string; text?: string; attachments: ReadonlyArray; - bodyRanges: ReadonlyArray; + bodyRanges?: ReadonlyArray; type: Proto.DataMessage.Quote.Type; }; @@ -190,7 +191,7 @@ export type ProcessedDelete = { targetSentTimestamp?: number; }; -export type ProcessedBodyRange = Proto.DataMessage.IBodyRange; +export type ProcessedBodyRange = RawBodyRange; export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate; diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 5518772ca..0ce2ef444 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -31,6 +31,7 @@ import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME'; import { SECOND, DurationInSeconds } from '../util/durations'; import type { AnyPaymentEvent } from '../types/Payment'; import { PaymentEventKind } from '../types/Payment'; +import { filterAndClean } from '../types/BodyRange'; const FLAGS = Proto.DataMessage.Flags; export const ATTACHMENT_MAX = 32; @@ -175,8 +176,7 @@ export function processQuote( thumbnail: processAttachment(attachment.thumbnail), }; }), - // We need to remove all of the extra stuff on these objects so serialize properly - bodyRanges: quote.bodyRanges?.map(item => ({ ...item })) ?? [], + bodyRanges: filterAndClean(quote.bodyRanges), type: quote.type || Proto.DataMessage.Quote.Type.NORMAL, }; } @@ -349,8 +349,7 @@ export function processDataMessage( isViewOnce: Boolean(message.isViewOnce), reaction: processReaction(message.reaction), delete: processDelete(message.delete), - // We need to remove all of the extra stuff on these objects so serialize properly - bodyRanges: message.bodyRanges?.map(item => ({ ...item })) ?? [], + bodyRanges: filterAndClean(message.bodyRanges), groupCallUpdate: dropNull(message.groupCallUpdate), storyContext: dropNull(message.storyContext), giftBadge: processGiftBadge(message.giftBadge), diff --git a/ts/types/BodyRange.ts b/ts/types/BodyRange.ts index 72d188db2..386409741 100644 --- a/ts/types/BodyRange.ts +++ b/ts/types/BodyRange.ts @@ -9,6 +9,7 @@ import { SignalService as Proto } from '../protobuf'; import * as log from '../logging/log'; import { assertDev } from '../util/assert'; import { missingCaseError } from '../util/missingCaseError'; +import type { ConversationType } from '../state/ducks/conversations'; // Cold storage of body ranges @@ -43,6 +44,10 @@ export namespace BodyRange { displayStyle: DisplayStyle; }; + export function isRawRange(range: BodyRange): range is RawBodyRange { + return isMention(range) || isFormatting(range); + } + // these overloads help inference along export function isMention( bodyRange: HydratedBodyRangeType @@ -122,6 +127,61 @@ export type RangeNode = BodyRange< } >; +// We drop unknown bodyRanges and remove extra stuff so they serialize properly +export function filterAndClean( + ranges: ReadonlyArray | undefined | null +): ReadonlyArray | undefined { + if (!ranges) { + return undefined; + } + + return ranges + .filter((range: Proto.DataMessage.IBodyRange): range is RawBodyRange => { + if (!isNumber(range.start)) { + log.warn('filterAndClean: Dropping bodyRange with non-number start'); + return false; + } + if (!isNumber(range.length)) { + log.warn('filterAndClean: Dropping bodyRange with non-number length'); + return false; + } + + if (range.mentionUuid) { + return true; + } + if (range.style) { + return true; + } + + log.warn('filterAndClean: Dropping unknown bodyRange'); + return false; + }) + .map(range => ({ ...range })); +} + +export function hydrateRanges( + ranges: ReadonlyArray> | undefined, + conversationSelector: (id: string) => ConversationType +): Array | undefined { + if (!ranges) { + return undefined; + } + + return ranges.filter(BodyRange.isRawRange).map(range => { + if (BodyRange.isMention(range)) { + const conversation = conversationSelector(range.mentionUuid); + + return { + ...range, + conversationID: conversation.id, + replacementText: conversation.title, + }; + } + + return range; + }); +} + /** * Insert a range into an existing range tree, splitting up the range if it intersects * with an existing range