Filter incoming bodyRanges, also filter before display

This commit is contained in:
Scott Nonnenberg 2023-04-11 17:16:46 -07:00 committed by GitHub
parent ec1246f60a
commit 4c9baaef80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 126 deletions

View file

@ -85,7 +85,7 @@ import {
} from '../Crypto'; } from '../Crypto';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import type { DraftBodyRangeMention } from '../types/BodyRange'; import type { DraftBodyRangeMention } from '../types/BodyRange';
import { BodyRange } from '../types/BodyRange'; import { BodyRange, hydrateRanges } from '../types/BodyRange';
import { migrateColor } from '../util/migrateColor'; import { migrateColor } from '../util/migrateColor';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
@ -1094,24 +1094,7 @@ export class ConversationModel extends window.Backbone
const draft = this.get('draft'); const draft = this.get('draft');
const rawBodyRanges = this.get('draftBodyRanges') || []; const rawBodyRanges = this.get('draftBodyRanges') || [];
const bodyRanges = rawBodyRanges.map(range => { const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
// 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);
});
if (draft) { if (draft) {
return { return {
@ -3902,24 +3885,7 @@ export class ConversationModel extends window.Backbone
} }
const rawBodyRanges = this.get('lastMessageBodyRanges') || []; const rawBodyRanges = this.get('lastMessageBodyRanges') || [];
const bodyRanges = rawBodyRanges.map(range => { const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
// 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 text = stripNewlinesForLeftPane(lastMessageText); const text = stripNewlinesForLeftPane(lastMessageText);
const prefix = this.get('lastMessagePrefix'); const prefix = this.get('lastMessagePrefix');

View file

@ -53,7 +53,7 @@ import * as expirationTimer from '../util/expirationTimer';
import { getUserLanguages } from '../util/userLanguages'; import { getUserLanguages } from '../util/userLanguages';
import type { ReactionType } from '../types/Reactions'; 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 reactionUtil from '../reactions/util';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
@ -1957,30 +1957,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
id, id,
attachments: quote.attachments.slice(), attachments: quote.attachments.slice(),
bodyRanges: quote.bodyRanges bodyRanges: quote.bodyRanges?.slice(),
.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),
// Just placeholder values for the fields // Just placeholder values for the fields
referencedMessageNotFound: false, referencedMessageNotFound: false,

View file

@ -48,7 +48,7 @@ import type {
HydratedBodyRangeMention, HydratedBodyRangeMention,
HydratedBodyRangesType, HydratedBodyRangesType,
} from '../../types/BodyRange'; } from '../../types/BodyRange';
import { BodyRange } from '../../types/BodyRange'; import { BodyRange, hydrateRanges } from '../../types/BodyRange';
import type { AssertProps } from '../../types/Util'; import type { AssertProps } from '../../types/Util';
import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import { getMentionsRegex } from '../../types/Message'; import { getMentionsRegex } from '../../types/Message';
@ -317,22 +317,9 @@ export const processBodyRanges = (
return undefined; return undefined;
} }
return bodyRanges return hydrateRanges(bodyRanges, options.conversationSelector)?.sort(
.map(range => { (a, b) => b.start - a.start
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);
}; };
export const extractHydratedMentions = ( export const extractHydratedMentions = (

View file

@ -28,11 +28,9 @@ import {
getConversationSelector, getConversationSelector,
} from './conversations'; } from './conversations';
import type { HydratedBodyRangeType } from '../../types/BodyRange'; import { hydrateRanges } from '../../types/BodyRange';
import { BodyRange } from '../../types/BodyRange';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
import { missingCaseError } from '../../util/missingCaseError';
export const getSearch = (state: StateType): SearchStateType => state.search; export const getSearch = (state: StateType): SearchStateType => state.search;
@ -178,7 +176,6 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
searchConversationId?: string, searchConversationId?: string,
targetedMessageId?: string targetedMessageId?: string
) => { ) => {
const bodyRanges = message.bodyRanges || [];
return { return {
from, from,
to, to,
@ -187,24 +184,8 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
conversationId: message.conversationId, conversationId: message.conversationId,
sentAt: message.sent_at, sentAt: message.sent_at,
snippet: message.snippet || '', snippet: message.snippet || '',
bodyRanges: bodyRanges.map((range): HydratedBodyRangeType => { bodyRanges:
// Hydrate user information on mention hydrateRanges(message.bodyRanges, conversationSelector) || [],
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);
}),
body: message.body || '', body: message.body || '',
isSelected: Boolean( isSelected: Boolean(

View file

@ -42,7 +42,7 @@ import {
reduceStorySendStatus, reduceStorySendStatus,
resolveStorySendStatus, resolveStorySendStatus,
} from '../../util/resolveStorySendStatus'; } from '../../util/resolveStorySendStatus';
import { BodyRange } from '../../types/BodyRange'; import { BodyRange, hydrateRanges } from '../../types/BodyRange';
export const getStoriesState = (state: StateType): StoriesStateType => export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories; state.stories;
@ -302,24 +302,10 @@ export const getStoryReplies = createSelector(
? me ? me
: conversationSelector(reply.sourceUuid || reply.source); : conversationSelector(reply.sourceUuid || reply.source);
const { bodyRanges } = reply;
return { return {
author: getAvatarData(conversation), author: getAvatarData(conversation),
...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']), ...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']),
bodyRanges: bodyRanges?.map(bodyRange => { bodyRanges: hydrateRanges(reply.bodyRanges, conversationSelector),
if (BodyRange.isMention(bodyRange)) {
const mentionConvo = conversationSelector(bodyRange.mentionUuid);
return {
...bodyRange,
conversationID: mentionConvo.id,
replacementText: mentionConvo.title,
};
}
return bodyRange;
}),
reactionEmoji: reply.storyReaction?.emoji, reactionEmoji: reply.storyReaction?.emoji,
contactNameColor: contactNameColorSelector( contactNameColor: contactNameColorSelector(
reply.conversationId, reply.conversationId,

View file

@ -202,7 +202,7 @@ describe('processDataMessage', () => {
thumbnail: PROCESSED_ATTACHMENT, thumbnail: PROCESSED_ATTACHMENT,
}, },
], ],
bodyRanges: [], bodyRanges: undefined,
type: 0, type: 0,
}); });
}); });

View file

@ -125,6 +125,7 @@ import { chunk } from '../util/iterables';
import { isOlderThan } from '../util/timestamp'; import { isOlderThan } from '../util/timestamp';
import { inspectUnknownFieldTags } from '../util/inspectProtobufs'; import { inspectUnknownFieldTags } from '../util/inspectProtobufs';
import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { filterAndClean } from '../types/BodyRange';
const GROUPV1_ID_LENGTH = 16; const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32; const GROUPV2_ID_LENGTH = 32;
@ -2121,8 +2122,8 @@ export default class MessageReceiver
const message: ProcessedDataMessage = { const message: ProcessedDataMessage = {
attachments, 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, preview,
canReplyToStory: Boolean(msg.allowsReplies), canReplyToStory: Boolean(msg.allowsReplies),
expireTimer: DurationInSeconds.DAY, expireTimer: DurationInSeconds.DAY,

View file

@ -9,6 +9,7 @@ import type { GiftBadgeStates } from '../components/conversation/Message';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
import type { DurationInSeconds } from '../util/durations'; import type { DurationInSeconds } from '../util/durations';
import type { AnyPaymentEvent } from '../types/Payment'; import type { AnyPaymentEvent } from '../types/Payment';
import type { RawBodyRange } from '../types/BodyRange';
export { export {
IdentityKeyType, IdentityKeyType,
@ -150,7 +151,7 @@ export type ProcessedQuote = {
authorUuid?: string; authorUuid?: string;
text?: string; text?: string;
attachments: ReadonlyArray<ProcessedQuoteAttachment>; attachments: ReadonlyArray<ProcessedQuoteAttachment>;
bodyRanges: ReadonlyArray<ProcessedBodyRange>; bodyRanges?: ReadonlyArray<ProcessedBodyRange>;
type: Proto.DataMessage.Quote.Type; type: Proto.DataMessage.Quote.Type;
}; };
@ -190,7 +191,7 @@ export type ProcessedDelete = {
targetSentTimestamp?: number; targetSentTimestamp?: number;
}; };
export type ProcessedBodyRange = Proto.DataMessage.IBodyRange; export type ProcessedBodyRange = RawBodyRange;
export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate; export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate;

View file

@ -31,6 +31,7 @@ import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
import { SECOND, DurationInSeconds } from '../util/durations'; import { SECOND, DurationInSeconds } from '../util/durations';
import type { AnyPaymentEvent } from '../types/Payment'; import type { AnyPaymentEvent } from '../types/Payment';
import { PaymentEventKind } from '../types/Payment'; import { PaymentEventKind } from '../types/Payment';
import { filterAndClean } from '../types/BodyRange';
const FLAGS = Proto.DataMessage.Flags; const FLAGS = Proto.DataMessage.Flags;
export const ATTACHMENT_MAX = 32; export const ATTACHMENT_MAX = 32;
@ -175,8 +176,7 @@ export function processQuote(
thumbnail: processAttachment(attachment.thumbnail), thumbnail: processAttachment(attachment.thumbnail),
}; };
}), }),
// We need to remove all of the extra stuff on these objects so serialize properly bodyRanges: filterAndClean(quote.bodyRanges),
bodyRanges: quote.bodyRanges?.map(item => ({ ...item })) ?? [],
type: quote.type || Proto.DataMessage.Quote.Type.NORMAL, type: quote.type || Proto.DataMessage.Quote.Type.NORMAL,
}; };
} }
@ -349,8 +349,7 @@ export function processDataMessage(
isViewOnce: Boolean(message.isViewOnce), isViewOnce: Boolean(message.isViewOnce),
reaction: processReaction(message.reaction), reaction: processReaction(message.reaction),
delete: processDelete(message.delete), delete: processDelete(message.delete),
// We need to remove all of the extra stuff on these objects so serialize properly bodyRanges: filterAndClean(message.bodyRanges),
bodyRanges: message.bodyRanges?.map(item => ({ ...item })) ?? [],
groupCallUpdate: dropNull(message.groupCallUpdate), groupCallUpdate: dropNull(message.groupCallUpdate),
storyContext: dropNull(message.storyContext), storyContext: dropNull(message.storyContext),
giftBadge: processGiftBadge(message.giftBadge), giftBadge: processGiftBadge(message.giftBadge),

View file

@ -9,6 +9,7 @@ import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { assertDev } from '../util/assert'; import { assertDev } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import type { ConversationType } from '../state/ducks/conversations';
// Cold storage of body ranges // Cold storage of body ranges
@ -43,6 +44,10 @@ export namespace BodyRange {
displayStyle: DisplayStyle; displayStyle: DisplayStyle;
}; };
export function isRawRange(range: BodyRange<object>): range is RawBodyRange {
return isMention(range) || isFormatting(range);
}
// these overloads help inference along // these overloads help inference along
export function isMention( export function isMention(
bodyRange: HydratedBodyRangeType 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<Proto.DataMessage.IBodyRange> | undefined | null
): ReadonlyArray<RawBodyRange> | 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<BodyRange<object>> | undefined,
conversationSelector: (id: string) => ConversationType
): Array<HydratedBodyRangeType> | 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 * Insert a range into an existing range tree, splitting up the range if it intersects
* with an existing range * with an existing range