Fix attachments duplication on message syncing with current device

pull/2137/head
audric 3 years ago
parent e97ac5d7c7
commit fc916ce94c

@ -59,7 +59,7 @@ module.exports = {
removeMessage, removeMessage,
getUnreadByConversation, getUnreadByConversation,
getUnreadCountByConversation, getUnreadCountByConversation,
getMessageBySender, getMessageBySenderAndSentAt,
getMessageBySenderAndServerTimestamp, getMessageBySenderAndServerTimestamp,
getMessageBySenderAndTimestamp, getMessageBySenderAndTimestamp,
getMessageIdsFromServerIds, getMessageIdsFromServerIds,
@ -2145,17 +2145,15 @@ function getMessageById(id) {
return jsonToObject(row.json); return jsonToObject(row.json);
} }
function getMessageBySender({ source, sourceDevice, sentAt }) { function getMessageBySenderAndSentAt({ source, sentAt }) {
const rows = globalInstance const rows = globalInstance
.prepare( .prepare(
`SELECT json FROM ${MESSAGES_TABLE} WHERE `SELECT json FROM ${MESSAGES_TABLE} WHERE
source = $source AND source = $source AND
sourceDevice = $sourceDevice AND
sent_at = $sent_at;` sent_at = $sent_at;`
) )
.all({ .all({
source, source,
sourceDevice,
sent_at: sentAt, sent_at: sentAt,
}); });

@ -1359,7 +1359,6 @@
@include color-svg('../images/x-16.svg', $color-gray-60); @include color-svg('../images/x-16.svg', $color-gray-60);
} }
// Module: Search Results // Module: Search Results
.module-search-results { .module-search-results {

@ -115,7 +115,7 @@ const channelsToMake = {
removeAllMessagesInConversation, removeAllMessagesInConversation,
getMessageCount, getMessageCount,
getMessageBySender, getMessageBySenderAndSentAt,
getMessageBySenderAndServerTimestamp, getMessageBySenderAndServerTimestamp,
getMessageBySenderAndTimestamp, getMessageBySenderAndTimestamp,
getMessageIdsFromServerIds, getMessageIdsFromServerIds,
@ -683,18 +683,15 @@ export async function getMessageById(
return new MessageModel(message); return new MessageModel(message);
} }
export async function getMessageBySender({ export async function getMessageBySenderAndSentAt({
source, source,
sourceDevice,
sentAt, sentAt,
}: { }: {
source: string; source: string;
sourceDevice: number;
sentAt: number; sentAt: number;
}): Promise<MessageModel | null> { }): Promise<MessageModel | null> {
const messages = await channels.getMessageBySender({ const messages = await channels.getMessageBySenderAndSentAt({
source, source,
sourceDevice,
sentAt, sentAt,
}); });
if (!messages || !messages.length) { if (!messages || !messages.length) {
@ -854,11 +851,11 @@ export async function getUnprocessedCount(): Promise<number> {
return channels.getUnprocessedCount(); return channels.getUnprocessedCount();
} }
export async function getAllUnprocessed(): Promise<any> { export async function getAllUnprocessed(): Promise<Array<UnprocessedParameter>> {
return channels.getAllUnprocessed(); return channels.getAllUnprocessed();
} }
export async function getUnprocessedById(id: string): Promise<any> { export async function getUnprocessedById(id: string): Promise<UnprocessedParameter | undefined> {
return channels.getUnprocessedById(id); return channels.getUnprocessedById(id);
} }
@ -870,6 +867,8 @@ export type UnprocessedParameter = {
attempts: number; attempts: number;
messageHash: string; messageHash: string;
senderIdentity?: string; senderIdentity?: string;
decrypted?: string; // added once the envelopes's content is decrypted with updateCache
source?: string; // added once the envelopes's content is decrypted with updateCache
}; };
export async function saveUnprocessed(data: UnprocessedParameter): Promise<string> { export async function saveUnprocessed(data: UnprocessedParameter): Promise<string> {

@ -715,18 +715,17 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const recipients = this.getRecipients(); const recipients = this.getRecipients();
const now = Date.now(); const now = Date.now();
const networkTimestamp = now - getLatestTimestampOffset();
window?.log?.info( window?.log?.info(
'Sending message to conversation', 'Sending message to conversation',
this.idForLogging(), this.idForLogging(),
'with timestamp', 'with networkTimestamp: ',
now networkTimestamp
); );
const editedQuote = _.isEmpty(quote) ? undefined : quote; const editedQuote = _.isEmpty(quote) ? undefined : quote;
const diffTimestamp = Date.now() - getLatestTimestampOffset();
const messageObject: MessageAttributesOptionals = { const messageObject: MessageAttributesOptionals = {
type: 'outgoing', type: 'outgoing',
body, body,
@ -734,7 +733,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
quote: editedQuote, quote: editedQuote,
preview, preview,
attachments, attachments,
sent_at: diffTimestamp, sent_at: networkTimestamp,
received_at: now, received_at: now,
expireTimer, expireTimer,
recipients, recipients,

@ -25,7 +25,7 @@ import {
} from '../interactions/conversations/unsendingInteractions'; } from '../interactions/conversations/unsendingInteractions';
import { SettingsKey } from '../data/settings-key'; import { SettingsKey } from '../data/settings-key';
export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) { export async function handleContentMessage(envelope: EnvelopePlus, messageHash: string) {
try { try {
const plaintext = await decrypt(envelope, envelope.content); const plaintext = await decrypt(envelope, envelope.content);
@ -329,7 +329,7 @@ function shouldDropBlockedUserMessage(content: SignalService.Content): boolean {
export async function innerHandleContentMessage( export async function innerHandleContentMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,
plaintext: ArrayBuffer, plaintext: ArrayBuffer,
messageHash?: string messageHash: string
): Promise<void> { ): Promise<void> {
try { try {
perfStart(`SignalService.Content.decode-${envelope.id}`); perfStart(`SignalService.Content.decode-${envelope.id}`);

@ -12,7 +12,10 @@ import { getConversationController } from '../session/conversations';
import { handleClosedGroupControlMessage } from './closedGroups'; import { handleClosedGroupControlMessage } from './closedGroups';
import { MessageModel } from '../models/message'; import { MessageModel } from '../models/message';
import { MessageModelType } from '../models/messageType'; import { MessageModelType } from '../models/messageType';
import { getMessageBySender, getMessageBySenderAndServerTimestamp } from '../../ts/data/data'; import {
getMessageBySenderAndSentAt,
getMessageBySenderAndServerTimestamp,
} from '../../ts/data/data';
import { ConversationModel, ConversationTypeEnum } from '../models/conversation'; import { ConversationModel, ConversationTypeEnum } from '../models/conversation';
import { allowOnlyOneAtATime } from '../session/utils/Promise'; import { allowOnlyOneAtATime } from '../session/utils/Promise';
import { toHex } from '../session/utils/String'; import { toHex } from '../session/utils/String';
@ -272,7 +275,7 @@ function isBodyEmpty(body: string) {
export async function handleDataMessage( export async function handleDataMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,
dataMessage: SignalService.IDataMessage, dataMessage: SignalService.IDataMessage,
messageHash?: string messageHash: string
): Promise<void> { ): Promise<void> {
// we handle group updates from our other devices in handleClosedGroupControlMessage() // we handle group updates from our other devices in handleClosedGroupControlMessage()
if (dataMessage.closedGroupControlMessage) { if (dataMessage.closedGroupControlMessage) {
@ -314,15 +317,10 @@ export async function handleDataMessage(
return removeFromCache(envelope); return removeFromCache(envelope);
} }
const ev: any = {};
if (isMe) {
// Data messages for medium groups don't arrive as sync messages. Instead, // Data messages for medium groups don't arrive as sync messages. Instead,
// linked devices poll for group messages independently, thus they need // linked devices poll for group messages independently, thus they need
// to recognise some of those messages at their own. // to recognise some of those messages at their own.
ev.type = 'sent'; const messageEventType: 'sent' | 'message' = isMe ? 'sent' : 'message';
} else {
ev.type = 'message';
}
if (envelope.senderIdentity) { if (envelope.senderIdentity) {
message.group = { message.group = {
@ -330,19 +328,22 @@ export async function handleDataMessage(
}; };
} }
ev.confirm = () => removeFromCache(envelope); const confirm = () => removeFromCache(envelope);
ev.data = { const data: MessageCreationData = {
source: senderPubKey, source: senderPubKey,
destination: isMe ? message.syncTarget : undefined, destination: isMe ? message.syncTarget : envelope.source,
sourceDevice: 1, sourceDevice: 1,
timestamp: _.toNumber(envelope.timestamp), timestamp: _.toNumber(envelope.timestamp),
receivedAt: envelope.receivedAt, receivedAt: envelope.receivedAt,
message, message,
messageHash, messageHash,
isPublic: false,
serverId: null,
serverTimestamp: null,
}; };
await handleMessageEvent(ev); // dataMessage await handleMessageEvent(messageEventType, data, confirm);
} }
type MessageDuplicateSearchType = { type MessageDuplicateSearchType = {
@ -354,8 +355,8 @@ type MessageDuplicateSearchType = {
export type MessageId = { export type MessageId = {
source: string; source: string;
serverId: number; serverId?: number | null;
serverTimestamp: number; serverTimestamp?: number | null;
sourceDevice: number; sourceDevice: number;
timestamp: number; timestamp: number;
message: MessageDuplicateSearchType; message: MessageDuplicateSearchType;
@ -364,7 +365,6 @@ const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
export async function isMessageDuplicate({ export async function isMessageDuplicate({
source, source,
sourceDevice,
timestamp, timestamp,
message, message,
serverTimestamp, serverTimestamp,
@ -372,6 +372,7 @@ export async function isMessageDuplicate({
// serverTimestamp is only used for opengroupv2 // serverTimestamp is only used for opengroupv2
try { try {
let result; let result;
if (serverTimestamp) { if (serverTimestamp) {
// first try to find a duplicate with the same serverTimestamp from this sender // first try to find a duplicate with the same serverTimestamp from this sender
@ -386,9 +387,8 @@ export async function isMessageDuplicate({
// but we consider that a user sending two messages with the same serverTimestamp is unlikely // but we consider that a user sending two messages with the same serverTimestamp is unlikely
return Boolean(result); return Boolean(result);
} }
result = await getMessageBySender({ result = await getMessageBySenderAndSentAt({
source, source,
sourceDevice,
sentAt: timestamp, sentAt: timestamp,
}); });
@ -442,21 +442,21 @@ async function handleProfileUpdate(
} }
} }
export interface MessageCreationData { export type MessageCreationData = {
timestamp: number; timestamp: number;
isPublic: boolean;
receivedAt: number; receivedAt: number;
sourceDevice: number; // always 1 isn't it? sourceDevice: number; // always 1 for Session
source: string; source: string;
serverId: number;
message: any; message: any;
serverTimestamp: any; isPublic: boolean;
serverId: number | null;
serverTimestamp: number | null;
// Needed for synced outgoing messages // Needed for synced outgoing messages
expirationStartTimestamp: any; // ??? expirationStartTimestamp?: any; // ???
destination: string; destination: string;
messageHash?: string; messageHash: string;
} };
export function initIncomingMessage(data: MessageCreationData): MessageModel { export function initIncomingMessage(data: MessageCreationData): MessageModel {
const { const {
@ -472,24 +472,24 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel {
} = data; } = data;
const messageGroupId = message?.group?.id; const messageGroupId = message?.group?.id;
let groupId = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; const groupIdWithPrefix = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null;
let groupId: string | undefined;
if (groupId) { if (groupIdWithPrefix) {
groupId = PubKey.removeTextSecurePrefixIfNeeded(groupId); groupId = PubKey.removeTextSecurePrefixIfNeeded(groupIdWithPrefix);
} }
const messageData: any = { const messageData: any = {
source, source,
sourceDevice, sourceDevice,
serverId, // + (not present below in `createSentMessage`) serverId,
sent_at: timestamp, sent_at: timestamp,
serverTimestamp, serverTimestamp,
received_at: receivedAt || Date.now(), received_at: receivedAt || Date.now(),
conversationId: groupId ?? source, conversationId: groupId ?? source,
type: 'incoming', type: 'incoming',
direction: 'incoming', // + direction: 'incoming', // +
unread: 1, // + unread: 1,
isPublic, // + isPublic,
messageHash: messageHash || null, messageHash: messageHash || null,
}; };
@ -519,17 +519,17 @@ function createSentMessage(data: MessageCreationData): MessageModel {
}; };
const messageGroupId = message?.group?.id; const messageGroupId = message?.group?.id;
let groupId = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; const groupIdWithPrefix = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null;
let groupId: string | undefined;
if (groupId) { if (groupIdWithPrefix) {
groupId = PubKey.removeTextSecurePrefixIfNeeded(groupId); groupId = PubKey.removeTextSecurePrefixIfNeeded(groupIdWithPrefix);
} }
const messageData = { const messageData = {
source: UserUtils.getOurPubKeyStrFromCache(), source: UserUtils.getOurPubKeyStrFromCache(),
sourceDevice, sourceDevice,
serverTimestamp, serverTimestamp: serverTimestamp || undefined,
serverId, serverId: serverId || undefined,
sent_at: timestamp, sent_at: timestamp,
received_at: isPublic ? receivedAt : now, received_at: isPublic ? receivedAt : now,
isPublic, isPublic,
@ -550,17 +550,13 @@ export function createMessage(data: MessageCreationData, isIncoming: boolean): M
} }
} }
export interface MessageEvent {
data: any;
type: string;
confirm: () => void;
}
// tslint:disable:cyclomatic-complexity max-func-body-length */ // tslint:disable:cyclomatic-complexity max-func-body-length */
export async function handleMessageEvent(event: MessageEvent): Promise<void> { async function handleMessageEvent(
const { data, confirm } = event; messageEventType: 'sent' | 'message',
data: MessageCreationData,
const isIncoming = event.type === 'message'; confirm: () => void
): Promise<void> {
const isIncoming = messageEventType === 'message';
if (!data || !data.message) { if (!data || !data.message) {
window?.log?.warn('Invalid data passed to handleMessageEvent.', event); window?.log?.warn('Invalid data passed to handleMessageEvent.', event);

@ -37,7 +37,7 @@ interface ReqOptions {
const incomingMessagePromises: Array<Promise<any>> = []; const incomingMessagePromises: Array<Promise<any>> = [];
async function handleEnvelope(envelope: EnvelopePlus, messageHash?: string) { async function handleEnvelope(envelope: EnvelopePlus, messageHash: string) {
if (envelope.content && envelope.content.length > 0) { if (envelope.content && envelope.content.length > 0) {
return handleContentMessage(envelope, messageHash); return handleContentMessage(envelope, messageHash);
} }
@ -69,7 +69,7 @@ class EnvelopeQueue {
const envelopeQueue = new EnvelopeQueue(); const envelopeQueue = new EnvelopeQueue();
function queueEnvelope(envelope: EnvelopePlus, messageHash?: string) { function queueEnvelope(envelope: EnvelopePlus, messageHash: string) {
const id = getEnvelopeId(envelope); const id = getEnvelopeId(envelope);
// window?.log?.info('queueing envelope', id); // window?.log?.info('queueing envelope', id);
@ -201,9 +201,9 @@ async function queueCached(item: any) {
if (decrypted) { if (decrypted) {
const payloadPlaintext = StringUtils.encode(decrypted, 'base64'); const payloadPlaintext = StringUtils.encode(decrypted, 'base64');
queueDecryptedEnvelope(envelope, payloadPlaintext); queueDecryptedEnvelope(envelope, payloadPlaintext, envelope.messageHash);
} else { } else {
queueEnvelope(envelope); queueEnvelope(envelope, envelope.messageHash);
} }
} catch (error) { } catch (error) {
window?.log?.error( window?.log?.error(
@ -227,11 +227,11 @@ async function queueCached(item: any) {
} }
} }
function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) { function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer, messageHash: string) {
const id = getEnvelopeId(envelope); const id = getEnvelopeId(envelope);
window?.log?.info('queueing decrypted envelope', id); window?.log?.info('queueing decrypted envelope', id);
const task = handleDecryptedEnvelope.bind(null, envelope, plaintext); const task = handleDecryptedEnvelope.bind(null, envelope, plaintext, messageHash);
const taskWithTimeout = createTaskWithTimeout(task, `queueEncryptedEnvelope ${id}`); const taskWithTimeout = createTaskWithTimeout(task, `queueEncryptedEnvelope ${id}`);
try { try {
envelopeQueue.add(taskWithTimeout); envelopeQueue.add(taskWithTimeout);
@ -243,13 +243,17 @@ function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) {
} }
} }
async function handleDecryptedEnvelope(envelope: EnvelopePlus, plaintext: ArrayBuffer) { async function handleDecryptedEnvelope(
envelope: EnvelopePlus,
plaintext: ArrayBuffer,
messageHash: string
) {
// if (this.stoppingProcessing) { // if (this.stoppingProcessing) {
// return Promise.resolve(); // return Promise.resolve();
// } // }
if (envelope.content) { if (envelope.content) {
await innerHandleContentMessage(envelope, plaintext); await innerHandleContentMessage(envelope, plaintext, messageHash);
} else { } else {
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -315,9 +319,10 @@ export async function handleOpenGroupV2Message(
expirationStartTimestamp: undefined, expirationStartTimestamp: undefined,
source: sender, source: sender,
message: dataMessage, message: dataMessage,
messageHash: '', // we do not care of a hash for an opengroup message
}; };
// WARNING this is important that the isMessageDuplicate is made in the conversation.queueJob // WARNING this is important that the isMessageDuplicate is made in the conversation.queueJob
const isDuplicate = await isMessageDuplicate(messageCreationData); const isDuplicate = await isMessageDuplicate({ ...messageCreationData });
if (isDuplicate) { if (isDuplicate) {
window?.log?.info('Received duplicate message. Dropping it.'); window?.log?.info('Received duplicate message. Dropping it.');

@ -96,7 +96,7 @@ export async function send(
// and the isDuplicate messages relies on sent_at timestamp to be valid. // and the isDuplicate messages relies on sent_at timestamp to be valid.
const found = await getMessageById(message.identifier); const found = await getMessageById(message.identifier);
// make sure to not update the send timestamp if this a currently syncing message // make sure to not update the sent timestamp if this a currently syncing message
if (found && !found.get('sentSync')) { if (found && !found.get('sentSync')) {
found.set({ sent_at: diffTimestamp }); found.set({ sent_at: diffTimestamp });
await found.commit(); await found.commit();

Loading…
Cancel
Save