2022-08-25 05:04:42 +00:00
|
|
|
// Copyright 2020-2022 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import PQueue from 'p-queue';
|
|
|
|
|
|
|
|
import type { ContactSyncEvent } from '../textsecure/messageReceiverEvents';
|
|
|
|
import type { ModifiedContactDetails } from '../textsecure/ContactsParser';
|
|
|
|
import { UUID } from '../types/UUID';
|
|
|
|
import * as Conversation from '../types/Conversation';
|
|
|
|
import * as Errors from '../types/errors';
|
|
|
|
import type { ValidateConversationType } from '../model-types.d';
|
|
|
|
import type { ConversationModel } from '../models/conversations';
|
|
|
|
import { validateConversation } from '../util/validateConversation';
|
|
|
|
import { strictAssert } from '../util/assert';
|
|
|
|
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
2022-08-26 22:26:38 +00:00
|
|
|
import { normalizeUuid } from '../util/normalizeUuid';
|
2022-08-25 05:04:42 +00:00
|
|
|
import * as log from '../logging/log';
|
|
|
|
|
|
|
|
// When true - we are running the very first storage and contact sync after
|
|
|
|
// linking.
|
|
|
|
let isInitialSync = false;
|
|
|
|
|
|
|
|
export function setIsInitialSync(newValue: boolean): void {
|
|
|
|
log.info(`setIsInitialSync(${newValue})`);
|
|
|
|
isInitialSync = newValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateConversationFromContactSync(
|
|
|
|
conversation: ConversationModel,
|
|
|
|
details: ModifiedContactDetails,
|
2022-09-13 00:52:55 +00:00
|
|
|
receivedAtCounter: number,
|
|
|
|
sentAt: number
|
2022-08-25 05:04:42 +00:00
|
|
|
): Promise<void> {
|
|
|
|
const { writeNewAttachmentData, deleteAttachmentData, doesAttachmentExist } =
|
|
|
|
window.Signal.Migrations;
|
|
|
|
|
|
|
|
conversation.set({
|
|
|
|
name: details.name,
|
|
|
|
inbox_position: details.inboxPosition,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update the conversation avatar only if new avatar exists and hash differs
|
|
|
|
const { avatar } = details;
|
|
|
|
if (avatar && avatar.data) {
|
|
|
|
const newAttributes = await Conversation.maybeUpdateAvatar(
|
|
|
|
conversation.attributes,
|
|
|
|
avatar.data,
|
|
|
|
{
|
|
|
|
writeNewAttachmentData,
|
|
|
|
deleteAttachmentData,
|
|
|
|
doesAttachmentExist,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
conversation.set(newAttributes);
|
|
|
|
} else {
|
|
|
|
const { attributes } = conversation;
|
|
|
|
if (attributes.avatar && attributes.avatar.path) {
|
|
|
|
await deleteAttachmentData(attributes.avatar.path);
|
|
|
|
}
|
|
|
|
conversation.set({ avatar: null });
|
|
|
|
}
|
|
|
|
|
|
|
|
// expireTimer isn't in Storage Service so we have to rely on contact sync.
|
2022-09-01 18:26:10 +00:00
|
|
|
await conversation.updateExpirationTimer(details.expireTimer, {
|
2022-09-06 23:52:07 +00:00
|
|
|
// Note: because it's our conversationId, this notification will be marked read. But
|
|
|
|
// setting this will make 'isSetByOther' check true.
|
2022-09-01 18:26:10 +00:00
|
|
|
source: window.ConversationController.getOurConversationId(),
|
|
|
|
receivedAt: receivedAtCounter,
|
|
|
|
fromSync: true,
|
|
|
|
isInitialSync,
|
2022-09-13 00:52:55 +00:00
|
|
|
reason: `contact sync (sent=${sentAt})`,
|
2022-09-01 18:26:10 +00:00
|
|
|
});
|
2022-08-25 05:04:42 +00:00
|
|
|
|
|
|
|
window.Whisper.events.trigger('incrementProgress');
|
|
|
|
}
|
|
|
|
|
|
|
|
const queue = new PQueue({ concurrency: 1 });
|
|
|
|
|
|
|
|
async function doContactSync({
|
|
|
|
contacts,
|
2022-08-26 22:26:38 +00:00
|
|
|
complete,
|
2022-08-25 05:04:42 +00:00
|
|
|
receivedAtCounter,
|
2022-09-13 00:52:55 +00:00
|
|
|
sentAt,
|
2022-08-25 05:04:42 +00:00
|
|
|
}: ContactSyncEvent): Promise<void> {
|
2022-08-26 22:26:38 +00:00
|
|
|
// iOS sets `syncMessage.contacts.complete` flag to `true` unconditionally
|
|
|
|
// and so we have to employ tricks to figure out whether the sync is full or
|
|
|
|
// partial. Thankfully, iOS sends only two kinds of contact syncs: full or
|
|
|
|
// local sync. Local sync is always a single our own contact so we can do an
|
|
|
|
// UUID check.
|
|
|
|
const isFullSync =
|
|
|
|
complete &&
|
|
|
|
!(
|
|
|
|
contacts.length === 1 &&
|
|
|
|
normalizeUuid(contacts[0].uuid, 'doContactSync') ===
|
|
|
|
window.storage.user.getUuid()?.toString()
|
|
|
|
);
|
|
|
|
|
2022-09-13 00:52:55 +00:00
|
|
|
const logId = `doContactSync(sent=${sentAt}, receivedAt=${receivedAtCounter}, isFullSync=${isFullSync})`;
|
2022-08-26 22:26:38 +00:00
|
|
|
log.info(`${logId}: got ${contacts.length} contacts`);
|
2022-08-25 05:04:42 +00:00
|
|
|
|
|
|
|
const updatedConversations = new Set<ConversationModel>();
|
|
|
|
|
|
|
|
let promises = new Array<Promise<void>>();
|
|
|
|
for (const details of contacts) {
|
|
|
|
const partialConversation: ValidateConversationType = {
|
|
|
|
e164: details.number,
|
|
|
|
uuid: UUID.cast(details.uuid),
|
|
|
|
type: 'private',
|
|
|
|
};
|
|
|
|
|
|
|
|
const validationError = validateConversation(partialConversation);
|
|
|
|
if (validationError) {
|
|
|
|
log.error(
|
2022-08-26 22:26:38 +00:00
|
|
|
`${logId}: Invalid contact received`,
|
2022-08-25 05:04:42 +00:00
|
|
|
Errors.toLogFormat(validationError)
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const conversation = window.ConversationController.maybeMergeContacts({
|
|
|
|
e164: details.number,
|
|
|
|
aci: details.uuid,
|
2022-08-26 22:26:38 +00:00
|
|
|
reason: logId,
|
2022-08-25 05:04:42 +00:00
|
|
|
});
|
|
|
|
strictAssert(conversation, 'need conversation to queue the job!');
|
|
|
|
|
|
|
|
// It's important to use queueJob here because we might update the expiration timer
|
|
|
|
// and we don't want conflicts with incoming message processing happening on the
|
|
|
|
// conversation queue.
|
2022-08-26 22:26:38 +00:00
|
|
|
const job = conversation.queueJob(`${logId}.set`, async () => {
|
|
|
|
try {
|
|
|
|
await updateConversationFromContactSync(
|
|
|
|
conversation,
|
|
|
|
details,
|
2022-09-13 00:52:55 +00:00
|
|
|
receivedAtCounter,
|
|
|
|
sentAt
|
2022-08-26 22:26:38 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
updatedConversations.add(conversation);
|
|
|
|
} catch (error) {
|
|
|
|
log.error(
|
|
|
|
'updateConversationFromContactSync error:',
|
|
|
|
Errors.toLogFormat(error)
|
|
|
|
);
|
2022-08-25 05:04:42 +00:00
|
|
|
}
|
2022-08-26 22:26:38 +00:00
|
|
|
});
|
2022-08-25 05:04:42 +00:00
|
|
|
|
|
|
|
promises.push(job);
|
|
|
|
}
|
|
|
|
|
|
|
|
// updatedConversations are not populated until the promises are resolved
|
|
|
|
await Promise.all(promises);
|
|
|
|
promises = [];
|
|
|
|
|
2022-08-26 22:26:38 +00:00
|
|
|
// Erase data in conversations that are not the part of contact sync only
|
|
|
|
// if we received a full contact sync (and not a one-off contact update).
|
|
|
|
const notUpdated = isFullSync
|
|
|
|
? window.ConversationController.getAll().filter(
|
|
|
|
convo =>
|
|
|
|
!updatedConversations.has(convo) &&
|
|
|
|
isDirectConversation(convo.attributes) &&
|
|
|
|
!isMe(convo.attributes)
|
|
|
|
)
|
|
|
|
: [];
|
2022-08-25 05:04:42 +00:00
|
|
|
|
|
|
|
log.info(
|
2022-08-26 22:26:38 +00:00
|
|
|
`${logId}: ` +
|
2022-08-25 05:04:42 +00:00
|
|
|
`updated ${updatedConversations.size} ` +
|
|
|
|
`resetting ${notUpdated.length}`
|
|
|
|
);
|
|
|
|
|
|
|
|
for (const conversation of notUpdated) {
|
|
|
|
conversation.set({
|
|
|
|
name: undefined,
|
|
|
|
inbox_position: undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save new conversation attributes
|
|
|
|
promises.push(
|
|
|
|
window.Signal.Data.updateConversations(
|
|
|
|
[...updatedConversations, ...notUpdated].map(convo => convo.attributes)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
await window.storage.put('synced_at', Date.now());
|
|
|
|
window.Whisper.events.trigger('contactSync:complete');
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function onContactSync(ev: ContactSyncEvent): Promise<void> {
|
2022-09-13 00:52:55 +00:00
|
|
|
log.info(
|
|
|
|
`onContactSync(sent=${ev.sentAt}, receivedAt=${ev.receivedAtCounter}): queueing sync`
|
|
|
|
);
|
2022-08-25 05:04:42 +00:00
|
|
|
await queue.add(() => doContactSync(ev));
|
|
|
|
}
|