Process incoming story messages

This commit is contained in:
Josh Perez 2022-03-04 16:14:52 -05:00 committed by GitHub
parent df7cdfacc7
commit eb91eb6fec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 4382 additions and 652 deletions

View file

@ -43,11 +43,6 @@ import * as expirationTimer from '../util/expirationTimer';
import type { ReactionType } from '../types/Reactions';
import { UUID, UUIDKind } from '../types/UUID';
import * as reactionUtil from '../reactions/util';
import {
copyStickerToAttachments,
savePackMetadata,
getStickerPackStatus,
} from '../types/Stickers';
import * as Stickers from '../types/Stickers';
import * as Errors from '../types/errors';
import * as EmbeddedContact from '../types/EmbeddedContact';
@ -99,6 +94,7 @@ import {
isKeyChange,
isMessageHistoryUnsynced,
isOutgoing,
isStory,
isProfileChange,
isTapToView,
isUniversalTimerNotification,
@ -124,7 +120,6 @@ import { ReactionSource } from '../reactions/ReactionSource';
import { ReadSyncs } from '../messageModifiers/ReadSyncs';
import { ViewSyncs } from '../messageModifiers/ViewSyncs';
import { ViewOnceOpenSyncs } from '../messageModifiers/ViewOnceOpenSyncs';
import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
import * as LinkPreview from '../types/LinkPreview';
import { SignalService as Proto } from '../protobuf';
import {
@ -141,13 +136,18 @@ import {
getContact,
getContactId,
getSource,
getSourceDevice,
getSourceUuid,
isCustomError,
isQuoteAMatch,
} from '../messages/helpers';
import type { ReplacementValuesType } from '../types/I18N';
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
import { getMessageIdForLogging } from '../util/getMessageIdForLogging';
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { findStoryMessage } from '../util/findStoryMessage';
import { isConversationAccepted } from '../util/isConversationAccepted';
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
/* eslint-disable camelcase */
@ -165,7 +165,7 @@ window.Whisper = window.Whisper || {};
const { Message: TypedMessage } = window.Signal.Types;
const { upgradeMessageSchema } = window.Signal.Migrations;
const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
const { getMessageBySender } = window.Signal.Data;
export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
static getLongMessageAttachment: (
@ -232,6 +232,33 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// Note: The clone is important for triggering a re-run of selectors
messageChanged(this.id, conversationId, { ...this.attributes });
}
const { addStory } = window.reduxActions.stories;
if (isStory(this.attributes)) {
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const storyData = getStoryDataFromMessageAttributes(
this.attributes,
ourConversationId
);
if (!storyData) {
return;
}
// TODO DESKTOP-3179
// Only add stories to redux if we've downloaded them. This should work
// because once we download a story we'll receive another change event
// which kicks off this function again.
if (Attachment.hasNotDownloaded(storyData.attachment)) {
return;
}
// This is fine to call multiple times since the addStory action only
// adds new stories.
addStory(storyData);
}
}
getSenderIdentifier(): string {
@ -740,12 +767,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// General
idForLogging(): string {
const account =
getSourceUuid(this.attributes) || getSource(this.attributes);
const device = getSourceDevice(this.attributes);
const timestamp = this.get('sent_at');
return `${account}.${device} ${timestamp}`;
return getMessageIdForLogging(this.attributes);
}
override defaults(): Partial<MessageAttributesType> {
@ -1636,332 +1658,18 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return getLastChallengeError(this.attributes);
}
// NOTE: If you're modifying this function then you'll likely also need
// to modify queueAttachmentDownloads since it contains the logic below
hasAttachmentDownloads(): boolean {
const attachments = this.get('attachments') || [];
const [longMessageAttachments, normalAttachments] = _.partition(
attachments,
attachment => MIME.isLongMessage(attachment.contentType)
);
if (longMessageAttachments.length > 0) {
return true;
}
const hasNormalAttachments = normalAttachments.some(attachment => {
if (!attachment) {
return false;
}
// We've already downloaded this!
if (attachment.path) {
return false;
}
return true;
});
if (hasNormalAttachments) {
return true;
}
const previews = this.get('preview') || [];
const hasPreviews = previews.some(item => {
if (!item.image) {
return false;
}
// We've already downloaded this!
if (item.image.path) {
return false;
}
return true;
});
if (hasPreviews) {
return true;
}
const contacts = this.get('contact') || [];
const hasContacts = contacts.some(item => {
if (!item.avatar || !item.avatar.avatar) {
return false;
}
if (item.avatar.avatar.path) {
return false;
}
return true;
});
if (hasContacts) {
return true;
}
const quote = this.get('quote');
const quoteAttachments =
quote && quote.attachments ? quote.attachments : [];
const hasQuoteAttachments = quoteAttachments.some(item => {
if (!item.thumbnail) {
return false;
}
// We've already downloaded this!
if (item.thumbnail.path) {
return false;
}
return true;
});
if (hasQuoteAttachments) {
return true;
}
const sticker = this.get('sticker');
if (sticker) {
return !sticker.data || (sticker.data && !sticker.data.path);
}
return false;
return hasAttachmentDownloads(this.attributes);
}
// Receive logic
// NOTE: If you're changing any logic in this function that deals with the
// count then you'll also have to modify the above function
// hasAttachmentDownloads
async queueAttachmentDownloads(): Promise<boolean> {
const attachmentsToQueue = this.get('attachments') || [];
const messageId = this.id;
let count = 0;
let bodyPending;
log.info(
`Queueing ${
attachmentsToQueue.length
} attachment downloads for message ${this.idForLogging()}`
);
const [longMessageAttachments, normalAttachments] = _.partition(
attachmentsToQueue,
attachment => MIME.isLongMessage(attachment.contentType)
);
if (longMessageAttachments.length > 1) {
log.error(
`Received more than one long message attachment in message ${this.idForLogging()}`
);
const value = await queueAttachmentDownloads(this.attributes);
if (!value) {
return false;
}
log.info(
`Queueing ${
longMessageAttachments.length
} long message attachment downloads for message ${this.idForLogging()}`
);
if (longMessageAttachments.length > 0) {
count += 1;
bodyPending = true;
await AttachmentDownloads.addJob(longMessageAttachments[0], {
messageId,
type: 'long-message',
index: 0,
});
}
log.info(
`Queueing ${
normalAttachments.length
} normal attachment downloads for message ${this.idForLogging()}`
);
const attachments = await Promise.all(
normalAttachments.map((attachment, index) => {
if (!attachment) {
return attachment;
}
// We've already downloaded this!
if (attachment.path) {
log.info(
`Normal attachment already downloaded for message ${this.idForLogging()}`
);
return attachment;
}
count += 1;
return AttachmentDownloads.addJob(attachment, {
messageId,
type: 'attachment',
index,
});
})
);
const previewsToQueue = this.get('preview') || [];
log.info(
`Queueing ${
previewsToQueue.length
} preview attachment downloads for message ${this.idForLogging()}`
);
const preview = await Promise.all(
previewsToQueue.map(async (item, index) => {
if (!item.image) {
return item;
}
// We've already downloaded this!
if (item.image.path) {
log.info(
`Preview attachment already downloaded for message ${this.idForLogging()}`
);
return item;
}
count += 1;
return {
...item,
image: await AttachmentDownloads.addJob(item.image, {
messageId,
type: 'preview',
index,
}),
};
})
);
const contactsToQueue = this.get('contact') || [];
log.info(
`Queueing ${
contactsToQueue.length
} contact attachment downloads for message ${this.idForLogging()}`
);
const contact = await Promise.all(
contactsToQueue.map(async (item, index) => {
if (!item.avatar || !item.avatar.avatar) {
return item;
}
// We've already downloaded this!
if (item.avatar.avatar.path) {
log.info(
`Contact attachment already downloaded for message ${this.idForLogging()}`
);
return item;
}
count += 1;
return {
...item,
avatar: {
...item.avatar,
avatar: await AttachmentDownloads.addJob(item.avatar.avatar, {
messageId,
type: 'contact',
index,
}),
},
};
})
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let quote = this.get('quote')!;
const quoteAttachmentsToQueue =
quote && quote.attachments ? quote.attachments : [];
log.info(
`Queueing ${
quoteAttachmentsToQueue.length
} quote attachment downloads for message ${this.idForLogging()}`
);
if (quoteAttachmentsToQueue.length > 0) {
quote = {
...quote,
attachments: await Promise.all(
(quote.attachments || []).map(async (item, index) => {
if (!item.thumbnail) {
return item;
}
// We've already downloaded this!
if (item.thumbnail.path) {
log.info(
`Quote attachment already downloaded for message ${this.idForLogging()}`
);
return item;
}
count += 1;
return {
...item,
thumbnail: await AttachmentDownloads.addJob(item.thumbnail, {
messageId,
type: 'quote',
index,
}),
};
})
),
};
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let sticker = this.get('sticker')!;
if (sticker && sticker.data && sticker.data.path) {
log.info(
`Sticker attachment already downloaded for message ${this.idForLogging()}`
);
} else if (sticker) {
log.info(`Queueing sticker download for message ${this.idForLogging()}`);
count += 1;
const { packId, stickerId, packKey } = sticker;
const status = getStickerPackStatus(packId);
let data: AttachmentType | undefined;
if (status && (status === 'downloaded' || status === 'installed')) {
try {
data = await copyStickerToAttachments(packId, stickerId);
} catch (error) {
log.error(
`Problem copying sticker (${packId}, ${stickerId}) to attachments:`,
error && error.stack ? error.stack : error
);
}
}
if (!data && sticker.data) {
data = await AttachmentDownloads.addJob(sticker.data, {
messageId,
type: 'sticker',
index: 0,
});
}
if (!status) {
// Save the packId/packKey for future download/install
savePackMetadata(packId, packKey, { messageId });
} else {
await addStickerPackReference(messageId, packId);
}
if (!data) {
throw new Error(
'queueAttachmentDownloads: Failed to fetch sticker data'
);
}
sticker = {
...sticker,
packId,
data,
};
}
log.info(
`Queued ${count} total attachment downloads for message ${this.idForLogging()}`
);
if (count > 0) {
this.set({
bodyPending,
attachments,
preview,
contact,
quote,
sticker,
});
return true;
}
return false;
this.set(value);
return true;
}
markAttachmentAsCorrupted(attachment: AttachmentType): void {
@ -2207,6 +1915,18 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
`Starting handleDataMessage for message ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
);
if (
type === 'story' &&
!isConversationAccepted(conversation.attributes)
) {
log.info(
'handleDataMessage: dropping story from !whitelisted',
this.getSenderIdentifier()
);
confirm();
return;
}
// First, check for duplicates. If we find one, stop processing here.
const inMemoryMessage = window.MessageController.findBySender(
this.getSenderIdentifier()
@ -2471,12 +2191,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
}
const [quote, storyQuote] = await Promise.all([
this.copyFromQuotedMessage(initialMessage.quote, conversation.id),
findStoryMessage(conversation.id, initialMessage.storyContext),
]);
const withQuoteReference = {
...initialMessage,
quote: await this.copyFromQuotedMessage(
initialMessage.quote,
conversation.id
),
quote,
storyId: storyQuote?.id,
};
const dataMessage = await upgradeMessageSchema(withQuoteReference);
@ -2521,6 +2244,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion,
sticker: dataMessage.sticker,
storyId: dataMessage.storyId,
});
const isSupported = !isUnsupportedMessage(message.attributes);
@ -2807,8 +2531,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
conversation.incrementMessageCount();
window.Signal.Data.updateConversation(conversation.attributes);
// Only queue attachments for downloads if this is an outgoing message
// or we've accepted the conversation
// Only queue attachments for downloads if this is a story or
// outgoing message or we've accepted the conversation
const reduxState = window.reduxStore.getState();
const attachments = this.get('attachments') || [];
const shouldHoldOffDownload =
@ -2818,6 +2542,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.hasAttachmentDownloads() &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(this.getConversation()!.getAccepted() ||
isStory(message.attributes) ||
isOutgoing(message.attributes)) &&
!shouldHoldOffDownload
) {