Early preparations for PNP Contact Merging
This commit is contained in:
parent
2f5dd73e58
commit
faf6c41332
30 changed files with 1572 additions and 447 deletions
|
@ -13,17 +13,107 @@ import type {
|
|||
import type { ConversationModel } from './models/conversations';
|
||||
import { getContactId } from './messages/helpers';
|
||||
import { maybeDeriveGroupV2Id } from './groups';
|
||||
import { assert } from './util/assert';
|
||||
import { assert, strictAssert } from './util/assert';
|
||||
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
||||
import { getConversationUnreadCountForAppBadge } from './util/getConversationUnreadCountForAppBadge';
|
||||
import { UUID, isValidUuid } from './types/UUID';
|
||||
import { UUID, isValidUuid, UUIDKind } from './types/UUID';
|
||||
import type { UUIDStringType } from './types/UUID';
|
||||
import { Address } from './types/Address';
|
||||
import { QualifiedAddress } from './types/QualifiedAddress';
|
||||
import * as log from './logging/log';
|
||||
import * as Errors from './types/errors';
|
||||
import { sleep } from './util/sleep';
|
||||
import { isNotNil } from './util/isNotNil';
|
||||
import { MINUTE, SECOND } from './util/durations';
|
||||
|
||||
type ConvoMatchType =
|
||||
| {
|
||||
key: 'uuid' | 'pni';
|
||||
value: UUIDStringType | undefined;
|
||||
match: ConversationModel | undefined;
|
||||
}
|
||||
| {
|
||||
key: 'e164';
|
||||
value: string | undefined;
|
||||
match: ConversationModel | undefined;
|
||||
};
|
||||
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
|
||||
function applyChangeToConversation(
|
||||
conversation: ConversationModel,
|
||||
suggestedChange: Partial<
|
||||
Pick<ConversationAttributesType, 'uuid' | 'e164' | 'pni'>
|
||||
>
|
||||
) {
|
||||
const change = { ...suggestedChange };
|
||||
|
||||
// Clear PNI if changing e164 without associated PNI
|
||||
if (hasOwnProperty.call(change, 'e164') && !change.pni) {
|
||||
change.pni = undefined;
|
||||
}
|
||||
|
||||
// If we have a PNI but not an ACI, then the PNI will go in the UUID field
|
||||
// Tricky: We need a special check here, because the PNI can be in the uuid slot
|
||||
if (
|
||||
change.pni &&
|
||||
!change.uuid &&
|
||||
(!conversation.get('uuid') ||
|
||||
conversation.get('uuid') === conversation.get('pni'))
|
||||
) {
|
||||
change.uuid = change.pni;
|
||||
}
|
||||
|
||||
// If we're clearing a PNI, but we didn't have an ACI - we need to clear UUID field
|
||||
if (
|
||||
!change.uuid &&
|
||||
hasOwnProperty.call(change, 'pni') &&
|
||||
!change.pni &&
|
||||
conversation.get('uuid') === conversation.get('pni')
|
||||
) {
|
||||
change.uuid = undefined;
|
||||
}
|
||||
|
||||
if (hasOwnProperty.call(change, 'uuid')) {
|
||||
conversation.updateUuid(change.uuid);
|
||||
}
|
||||
if (hasOwnProperty.call(change, 'e164')) {
|
||||
conversation.updateE164(change.e164);
|
||||
}
|
||||
if (hasOwnProperty.call(change, 'pni')) {
|
||||
conversation.updatePni(change.pni);
|
||||
}
|
||||
|
||||
// Note: we don't do a conversation.set here, because change is limited to these fields
|
||||
}
|
||||
|
||||
async function mergeConversations({
|
||||
logId,
|
||||
oldConversation,
|
||||
newConversation,
|
||||
}: {
|
||||
logId: string;
|
||||
oldConversation: ConversationModel;
|
||||
newConversation: ConversationModel;
|
||||
}) {
|
||||
try {
|
||||
await window.ConversationController.combineConversations(
|
||||
newConversation,
|
||||
oldConversation
|
||||
);
|
||||
|
||||
// If the old conversation was currently displayed, we load the new one
|
||||
window.Whisper.events.trigger('refreshConversation', {
|
||||
newId: newConversation.get('id'),
|
||||
oldId: oldConversation.get('id'),
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
`${logId}: error combining contacts: ${Errors.toLogFormat(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||
|
||||
const {
|
||||
|
@ -55,6 +145,8 @@ export class ConversationController {
|
|||
|
||||
private _hasQueueEmptied = false;
|
||||
|
||||
private _combineConversationsQueue = new PQueue({ concurrency: 1 });
|
||||
|
||||
constructor(private _conversations: ConversationModelCollectionType) {
|
||||
const debouncedUpdateUnreadCount = debounce(
|
||||
this.updateUnreadCount.bind(this),
|
||||
|
@ -172,8 +264,8 @@ export class ConversationController {
|
|||
if (type === 'group') {
|
||||
conversation = this._conversations.add({
|
||||
id,
|
||||
uuid: null,
|
||||
e164: null,
|
||||
uuid: undefined,
|
||||
e164: undefined,
|
||||
groupId: identifier,
|
||||
type,
|
||||
version: 2,
|
||||
|
@ -183,8 +275,8 @@ export class ConversationController {
|
|||
conversation = this._conversations.add({
|
||||
id,
|
||||
uuid: identifier,
|
||||
e164: null,
|
||||
groupId: null,
|
||||
e164: undefined,
|
||||
groupId: undefined,
|
||||
type,
|
||||
version: 2,
|
||||
...additionalInitialProps,
|
||||
|
@ -192,9 +284,9 @@ export class ConversationController {
|
|||
} else {
|
||||
conversation = this._conversations.add({
|
||||
id,
|
||||
uuid: null,
|
||||
uuid: undefined,
|
||||
e164: identifier,
|
||||
groupId: null,
|
||||
groupId: undefined,
|
||||
type,
|
||||
version: 2,
|
||||
...additionalInitialProps,
|
||||
|
@ -270,13 +362,25 @@ export class ConversationController {
|
|||
|
||||
getOurConversationId(): string | undefined {
|
||||
const e164 = window.textsecure.storage.user.getNumber();
|
||||
const uuid = window.textsecure.storage.user.getUuid()?.toString();
|
||||
return this.ensureContactIds({
|
||||
const aci = window.textsecure.storage.user
|
||||
.getUuid(UUIDKind.ACI)
|
||||
?.toString();
|
||||
const pni = window.textsecure.storage.user
|
||||
.getUuid(UUIDKind.PNI)
|
||||
?.toString();
|
||||
|
||||
if (!e164 && !aci && !pni) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversation = this.maybeMergeContacts({
|
||||
aci,
|
||||
e164,
|
||||
uuid,
|
||||
highTrust: true,
|
||||
pni,
|
||||
reason: 'getOurConversationId',
|
||||
});
|
||||
|
||||
return conversation?.id;
|
||||
}
|
||||
|
||||
getOurConversationIdOrThrow(): string {
|
||||
|
@ -311,39 +415,210 @@ export class ConversationController {
|
|||
return ourDeviceId === 1;
|
||||
}
|
||||
|
||||
// Note: If you don't know what kind of UUID it is, put it in the 'aci' param.
|
||||
maybeMergeContacts({
|
||||
aci: providedAci,
|
||||
e164,
|
||||
pni: providedPni,
|
||||
reason,
|
||||
mergeOldAndNew = mergeConversations,
|
||||
}: {
|
||||
aci?: string;
|
||||
e164?: string;
|
||||
pni?: string;
|
||||
reason: string;
|
||||
recursionCount?: number;
|
||||
mergeOldAndNew?: (options: {
|
||||
logId: string;
|
||||
oldConversation: ConversationModel;
|
||||
newConversation: ConversationModel;
|
||||
}) => Promise<void>;
|
||||
}): ConversationModel | undefined {
|
||||
const dataProvided = [];
|
||||
if (providedAci) {
|
||||
dataProvided.push('aci');
|
||||
}
|
||||
if (e164) {
|
||||
dataProvided.push('e164');
|
||||
}
|
||||
if (providedPni) {
|
||||
dataProvided.push('pni');
|
||||
}
|
||||
const logId = `maybeMergeContacts/${reason}/${dataProvided.join('+')}`;
|
||||
|
||||
const aci = providedAci ? UUID.cast(providedAci) : undefined;
|
||||
const pni = providedPni ? UUID.cast(providedPni) : undefined;
|
||||
|
||||
if (!aci && !e164 && !pni) {
|
||||
throw new Error(
|
||||
`${logId}: Need to provide at least one of: aci, e164, pni`
|
||||
);
|
||||
}
|
||||
|
||||
if (pni && !e164) {
|
||||
throw new Error(`${logId}: Cannot provide pni without an e164`);
|
||||
}
|
||||
|
||||
const identifier = aci || e164 || pni;
|
||||
strictAssert(identifier, `${logId}: identifier must be truthy!`);
|
||||
|
||||
const matches: Array<ConvoMatchType> = [
|
||||
{
|
||||
key: 'uuid',
|
||||
value: aci,
|
||||
match: window.ConversationController.get(aci),
|
||||
},
|
||||
{
|
||||
key: 'e164',
|
||||
value: e164,
|
||||
match: window.ConversationController.get(e164),
|
||||
},
|
||||
{ key: 'pni', value: pni, match: window.ConversationController.get(pni) },
|
||||
];
|
||||
let unusedMatches: Array<ConvoMatchType> = [];
|
||||
|
||||
let targetConversation: ConversationModel | undefined;
|
||||
let matchCount = 0;
|
||||
matches.forEach(item => {
|
||||
const { key, value, match } = item;
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
if (targetConversation) {
|
||||
log.info(
|
||||
`${logId}: No match for ${key}, applying to target conversation`
|
||||
);
|
||||
// Note: This line might erase a known e164 or PNI
|
||||
applyChangeToConversation(targetConversation, {
|
||||
[key]: value,
|
||||
});
|
||||
} else {
|
||||
unusedMatches.push(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
matchCount += 1;
|
||||
unusedMatches.forEach(unused => {
|
||||
strictAssert(unused.value, 'An unused value should always be truthy');
|
||||
|
||||
// Example: If we find that our PNI match has no ACI, then it will be our target.
|
||||
// Tricky: PNI can end up in UUID slot, so we need to special-case it
|
||||
if (
|
||||
!targetConversation &&
|
||||
(!match.get(unused.key) ||
|
||||
(unused.key === 'uuid' && match.get(unused.key) === pni))
|
||||
) {
|
||||
log.info(
|
||||
`${logId}: Match on ${key} does not have ${unused.key}, ` +
|
||||
`so it will be our target conversation - ${match.idForLogging()}`
|
||||
);
|
||||
targetConversation = match;
|
||||
}
|
||||
|
||||
// If PNI match already has an ACI, then we need to create a new one
|
||||
if (!targetConversation) {
|
||||
targetConversation = this.getOrCreate(unused.value, 'private');
|
||||
log.info(
|
||||
`${logId}: Match on ${key} already had ${unused.key}, ` +
|
||||
`so created new target conversation - ${targetConversation.idForLogging()}`
|
||||
);
|
||||
}
|
||||
|
||||
log.info(
|
||||
`${logId}: Applying new value for ${unused.key} to target conversation`
|
||||
);
|
||||
applyChangeToConversation(targetConversation, {
|
||||
[unused.key]: unused.value,
|
||||
});
|
||||
});
|
||||
|
||||
unusedMatches = [];
|
||||
|
||||
if (targetConversation && targetConversation !== match) {
|
||||
// Clear the value on the current match, since it belongs on targetConversation!
|
||||
// Note: we need to do the remove first, because it will clear the lookup!
|
||||
log.info(
|
||||
`${logId}: Clearing ${key} on match, and adding it to target conversation`
|
||||
);
|
||||
const change: Pick<
|
||||
Partial<ConversationAttributesType>,
|
||||
'uuid' | 'e164' | 'pni'
|
||||
> = {
|
||||
[key]: undefined,
|
||||
};
|
||||
// When the PNI is being used in the uuid field alone, we need to clear it
|
||||
if (key === 'pni' && match.get('uuid') === pni) {
|
||||
change.uuid = undefined;
|
||||
}
|
||||
applyChangeToConversation(match, change);
|
||||
|
||||
applyChangeToConversation(targetConversation, {
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
// Note: The PNI check here is just to be bulletproof; if we know a UUID is a PNI,
|
||||
// then that should be put in the UUID field as well!
|
||||
if (!match.get('uuid') && !match.get('e164') && !match.get('pni')) {
|
||||
log.warn(
|
||||
`${logId}: Removing old conversation which matched on ${key}. ` +
|
||||
'Merging with target conversation.'
|
||||
);
|
||||
mergeOldAndNew({
|
||||
logId,
|
||||
oldConversation: match,
|
||||
newConversation: targetConversation,
|
||||
});
|
||||
}
|
||||
} else if (targetConversation && !targetConversation?.get(key)) {
|
||||
// This is mostly for the situation where PNI was erased when updating e164
|
||||
// log.debug(`${logId}: Re-adding ${key} on target conversation`);
|
||||
applyChangeToConversation(targetConversation, {
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
if (!targetConversation) {
|
||||
// log.debug(
|
||||
// `${logId}: Match on ${key} is target conversation - ${match.idForLogging()}`
|
||||
// );
|
||||
targetConversation = match;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetConversation) {
|
||||
return targetConversation;
|
||||
}
|
||||
|
||||
strictAssert(
|
||||
matchCount === 0,
|
||||
`${logId}: should be no matches if no targetConversation`
|
||||
);
|
||||
|
||||
log.info(`${logId}: Creating a new conversation with all inputs`);
|
||||
return this.getOrCreate(identifier, 'private', { e164, pni });
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a UUID and/or an E164, resolves to a string representing the local
|
||||
* database id of the given contact. In high trust mode, it may create new contacts,
|
||||
* and it may merge contacts.
|
||||
*
|
||||
* highTrust = uuid/e164 pairing came from CDS, the server, or your own device
|
||||
* Given a UUID and/or an E164, returns a string representing the local
|
||||
* database id of the given contact. Will create a new conversation if none exists;
|
||||
* otherwise will return whatever is found.
|
||||
*/
|
||||
ensureContactIds({
|
||||
lookupOrCreate({
|
||||
e164,
|
||||
uuid,
|
||||
highTrust,
|
||||
reason,
|
||||
}:
|
||||
| {
|
||||
e164?: string | null;
|
||||
uuid?: string | null;
|
||||
highTrust?: false;
|
||||
reason?: void;
|
||||
}
|
||||
| {
|
||||
e164?: string | null;
|
||||
uuid?: string | null;
|
||||
highTrust: true;
|
||||
reason: string;
|
||||
}): string | undefined {
|
||||
// Check for at least one parameter being provided. This is necessary
|
||||
// because this path can be called on startup to resolve our own ID before
|
||||
// our phone number or UUID are known. The existing behavior in these
|
||||
// cases can handle a returned `undefined` id, so we do that.
|
||||
}: {
|
||||
e164?: string | null;
|
||||
uuid?: string | null;
|
||||
}): ConversationModel | undefined {
|
||||
const normalizedUuid = uuid ? uuid.toLowerCase() : undefined;
|
||||
const identifier = normalizedUuid || e164;
|
||||
|
||||
if ((!e164 && !uuid) || !identifier) {
|
||||
log.warn('lookupOrCreate: Called with neither e164 nor uuid!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -352,147 +627,52 @@ export class ConversationController {
|
|||
|
||||
// 1. Handle no match at all
|
||||
if (!convoE164 && !convoUuid) {
|
||||
log.info(
|
||||
'ensureContactIds: Creating new contact, no matches found',
|
||||
highTrust ? reason : 'no reason'
|
||||
);
|
||||
log.info('lookupOrCreate: Creating new contact, no matches found');
|
||||
const newConvo = this.getOrCreate(identifier, 'private');
|
||||
if (highTrust && e164) {
|
||||
|
||||
// `identifier` would resolve to uuid if we had both, so fix up e164
|
||||
if (normalizedUuid && e164) {
|
||||
newConvo.updateE164(e164);
|
||||
}
|
||||
if (normalizedUuid) {
|
||||
newConvo.updateUuid(normalizedUuid);
|
||||
}
|
||||
if ((highTrust && e164) || normalizedUuid) {
|
||||
updateConversation(newConvo.attributes);
|
||||
}
|
||||
|
||||
return newConvo.get('id');
|
||||
|
||||
// 2. Handle match on only E164
|
||||
return newConvo;
|
||||
}
|
||||
if (convoE164 && !convoUuid) {
|
||||
const haveUuid = Boolean(normalizedUuid);
|
||||
log.info(
|
||||
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
|
||||
);
|
||||
// If we are only searching based on e164 anyway, then return the first result
|
||||
if (!normalizedUuid) {
|
||||
return convoE164.get('id');
|
||||
}
|
||||
|
||||
// Fill in the UUID for an e164-only contact
|
||||
if (normalizedUuid && !convoE164.get('uuid')) {
|
||||
if (highTrust) {
|
||||
log.info(
|
||||
`ensureContactIds: Adding UUID (${uuid}) to e164-only match ` +
|
||||
`(${e164}), reason: ${reason}`
|
||||
);
|
||||
convoE164.updateUuid(normalizedUuid);
|
||||
updateConversation(convoE164.attributes);
|
||||
}
|
||||
return convoE164.get('id');
|
||||
}
|
||||
|
||||
log.info(
|
||||
'ensureContactIds: e164 already had UUID, creating a new contact'
|
||||
);
|
||||
// If existing e164 match already has UUID, create a new contact...
|
||||
const newConvo = this.getOrCreate(normalizedUuid, 'private');
|
||||
|
||||
if (highTrust) {
|
||||
log.info(
|
||||
`ensureContactIds: Moving e164 (${e164}) from old contact ` +
|
||||
`(${convoE164.get('uuid')}) to new (${uuid}), reason: ${reason}`
|
||||
);
|
||||
|
||||
// Remove the e164 from the old contact...
|
||||
convoE164.set({ e164: undefined });
|
||||
updateConversation(convoE164.attributes);
|
||||
|
||||
// ...and add it to the new one.
|
||||
newConvo.updateE164(e164);
|
||||
updateConversation(newConvo.attributes);
|
||||
}
|
||||
|
||||
return newConvo.get('id');
|
||||
|
||||
// 3. Handle match on only UUID
|
||||
}
|
||||
// 2. Handle match on only UUID
|
||||
if (!convoE164 && convoUuid) {
|
||||
if (e164 && highTrust) {
|
||||
log.info(
|
||||
`ensureContactIds: Adding e164 (${e164}) to UUID-only match ` +
|
||||
`(${uuid}), reason: ${reason}`
|
||||
);
|
||||
convoUuid.updateE164(e164);
|
||||
updateConversation(convoUuid.attributes);
|
||||
}
|
||||
return convoUuid.get('id');
|
||||
return convoUuid;
|
||||
}
|
||||
|
||||
// 3. Handle match on only E164
|
||||
if (convoE164 && !convoUuid) {
|
||||
return convoE164;
|
||||
}
|
||||
|
||||
// For some reason, TypeScript doesn't believe that we can trust that these two values
|
||||
// are truthy by this point. So we'll throw if we get there.
|
||||
// are truthy by this point. So we'll throw if that isn't the case.
|
||||
if (!convoE164 || !convoUuid) {
|
||||
throw new Error('ensureContactIds: convoE164 or convoUuid are falsey!');
|
||||
}
|
||||
|
||||
// Now, we know that we have a match for both e164 and uuid checks
|
||||
|
||||
if (convoE164 === convoUuid) {
|
||||
return convoUuid.get('id');
|
||||
}
|
||||
|
||||
if (highTrust) {
|
||||
// Conflict: If e164 match already has a UUID, we remove its e164.
|
||||
if (convoE164.get('uuid') && convoE164.get('uuid') !== normalizedUuid) {
|
||||
log.info(
|
||||
`ensureContactIds: e164 match (${e164}) had different ` +
|
||||
`UUID(${convoE164.get('uuid')}) than incoming pair (${uuid}), ` +
|
||||
`removing its e164, reason: ${reason}`
|
||||
);
|
||||
|
||||
// Remove the e164 from the old contact...
|
||||
convoE164.set({ e164: undefined });
|
||||
updateConversation(convoE164.attributes);
|
||||
|
||||
// ...and add it to the new one.
|
||||
convoUuid.updateE164(e164);
|
||||
updateConversation(convoUuid.attributes);
|
||||
|
||||
return convoUuid.get('id');
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`ensureContactIds: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Merging.`
|
||||
throw new Error(
|
||||
'lookupOrCreate: convoE164 or convoUuid are falsey but should both be true!'
|
||||
);
|
||||
|
||||
// Conflict: If e164 match has no UUID, we merge. We prefer the UUID match.
|
||||
// Note: no await here, we want to keep this function synchronous
|
||||
convoUuid.updateE164(e164);
|
||||
// `then` is used to trigger async updates, not affecting return value
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.combineConversations(convoUuid, convoE164)
|
||||
.then(() => {
|
||||
// If the old conversation was currently displayed, we load the new one
|
||||
window.Whisper.events.trigger('refreshConversation', {
|
||||
newId: convoUuid.get('id'),
|
||||
oldId: convoE164.get('id'),
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
const errorText = error && error.stack ? error.stack : error;
|
||||
log.warn(`ensureContactIds error combining contacts: ${errorText}`);
|
||||
});
|
||||
}
|
||||
|
||||
return convoUuid.get('id');
|
||||
// 4. If the two lookups agree, return that conversation
|
||||
if (convoE164 === convoUuid) {
|
||||
return convoUuid;
|
||||
}
|
||||
|
||||
// 5. If the two lookups disagree, log and return the UUID match
|
||||
log.warn(
|
||||
`lookupOrCreate: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Returning UUID match.`
|
||||
);
|
||||
return convoUuid;
|
||||
}
|
||||
|
||||
async checkForConflicts(): Promise<void> {
|
||||
log.info('checkForConflicts: starting...');
|
||||
const byUuid = Object.create(null);
|
||||
const byE164 = Object.create(null);
|
||||
const byPni = Object.create(null);
|
||||
const byGroupV2Id = Object.create(null);
|
||||
// We also want to find duplicate GV1 IDs. You might expect to see a "byGroupV1Id" map
|
||||
// here. Instead, we check for duplicates on the derived GV2 ID.
|
||||
|
@ -509,6 +689,7 @@ export class ConversationController {
|
|||
);
|
||||
|
||||
const uuid = conversation.get('uuid');
|
||||
const pni = conversation.get('pni');
|
||||
const e164 = conversation.get('e164');
|
||||
|
||||
if (uuid) {
|
||||
|
@ -532,6 +713,27 @@ export class ConversationController {
|
|||
}
|
||||
}
|
||||
|
||||
if (pni) {
|
||||
const existing = byPni[pni];
|
||||
if (!existing) {
|
||||
byPni[pni] = conversation;
|
||||
} else {
|
||||
log.warn(`checkForConflicts: Found conflict with pni ${pni}`);
|
||||
|
||||
// Keep the newer one if it has a uuid, otherwise keep existing
|
||||
if (conversation.get('uuid')) {
|
||||
// Keep new one
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.combineConversations(conversation, existing);
|
||||
byPni[pni] = conversation;
|
||||
} else {
|
||||
// Keep existing - note that this applies if neither had an e164
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.combineConversations(existing, conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e164) {
|
||||
const existing = byE164[e164];
|
||||
if (!existing) {
|
||||
|
@ -619,100 +821,110 @@ export class ConversationController {
|
|||
current: ConversationModel,
|
||||
obsolete: ConversationModel
|
||||
): Promise<void> {
|
||||
const conversationType = current.get('type');
|
||||
return this._combineConversationsQueue.add(async () => {
|
||||
const conversationType = current.get('type');
|
||||
|
||||
if (obsolete.get('type') !== conversationType) {
|
||||
assert(
|
||||
false,
|
||||
'combineConversations cannot combine a private and group conversation. Doing nothing'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const obsoleteId = obsolete.get('id');
|
||||
const obsoleteUuid = obsolete.getUuid();
|
||||
const currentId = current.get('id');
|
||||
log.warn('combineConversations: Combining two conversations', {
|
||||
obsolete: obsoleteId,
|
||||
current: currentId,
|
||||
});
|
||||
|
||||
if (conversationType === 'private' && obsoleteUuid) {
|
||||
if (!current.get('profileKey') && obsolete.get('profileKey')) {
|
||||
if (!this.get(obsolete.id)) {
|
||||
log.warn(
|
||||
'combineConversations: Copying profile key from old to new contact'
|
||||
`combineConversations: Already combined obsolete conversation ${obsolete.id}`
|
||||
);
|
||||
|
||||
const profileKey = obsolete.get('profileKey');
|
||||
|
||||
if (profileKey) {
|
||||
await current.setProfileKey(profileKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Delete all sessions tied to old conversationId'
|
||||
);
|
||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
|
||||
ourUuid,
|
||||
identifier: obsoleteUuid.toString(),
|
||||
if (obsolete.get('type') !== conversationType) {
|
||||
assert(
|
||||
false,
|
||||
'combineConversations cannot combine a private and group conversation. Doing nothing'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const obsoleteId = obsolete.get('id');
|
||||
const obsoleteUuid = obsolete.getUuid();
|
||||
const currentId = current.get('id');
|
||||
log.warn('combineConversations: Combining two conversations', {
|
||||
obsolete: obsoleteId,
|
||||
current: currentId,
|
||||
});
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const addr = new QualifiedAddress(
|
||||
ourUuid,
|
||||
new Address(obsoleteUuid, deviceId)
|
||||
|
||||
if (conversationType === 'private' && obsoleteUuid) {
|
||||
if (!current.get('profileKey') && obsolete.get('profileKey')) {
|
||||
log.warn(
|
||||
'combineConversations: Copying profile key from old to new contact'
|
||||
);
|
||||
await window.textsecure.storage.protocol.removeSession(addr);
|
||||
})
|
||||
);
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Delete all identity information tied to old conversationId'
|
||||
);
|
||||
const profileKey = obsolete.get('profileKey');
|
||||
|
||||
if (obsoleteUuid) {
|
||||
await window.textsecure.storage.protocol.removeIdentityKey(
|
||||
obsoleteUuid
|
||||
if (profileKey) {
|
||||
await current.setProfileKey(profileKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Delete all sessions tied to old conversationId'
|
||||
);
|
||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||
{
|
||||
ourUuid,
|
||||
identifier: obsoleteUuid.toString(),
|
||||
}
|
||||
);
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const addr = new QualifiedAddress(
|
||||
ourUuid,
|
||||
new Address(obsoleteUuid, deviceId)
|
||||
);
|
||||
await window.textsecure.storage.protocol.removeSession(addr);
|
||||
})
|
||||
);
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Delete all identity information tied to old conversationId'
|
||||
);
|
||||
|
||||
if (obsoleteUuid) {
|
||||
await window.textsecure.storage.protocol.removeIdentityKey(
|
||||
obsoleteUuid
|
||||
);
|
||||
}
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Ensure that all V1 groups have new conversationId instead of old'
|
||||
);
|
||||
const groups = await this.getAllGroupsInvolvingUuid(obsoleteUuid);
|
||||
groups.forEach(group => {
|
||||
const members = group.get('members');
|
||||
const withoutObsolete = without(members, obsoleteId);
|
||||
const currentAdded = uniq([...withoutObsolete, currentId]);
|
||||
|
||||
group.set({
|
||||
members: currentAdded,
|
||||
});
|
||||
updateConversation(group.attributes);
|
||||
});
|
||||
}
|
||||
|
||||
// Note: we explicitly don't want to update V2 groups
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Ensure that all V1 groups have new conversationId instead of old'
|
||||
'combineConversations: Delete the obsolete conversation from the database'
|
||||
);
|
||||
const groups = await this.getAllGroupsInvolvingUuid(obsoleteUuid);
|
||||
groups.forEach(group => {
|
||||
const members = group.get('members');
|
||||
const withoutObsolete = without(members, obsoleteId);
|
||||
const currentAdded = uniq([...withoutObsolete, currentId]);
|
||||
await removeConversation(obsoleteId);
|
||||
|
||||
group.set({
|
||||
members: currentAdded,
|
||||
});
|
||||
updateConversation(group.attributes);
|
||||
log.warn('combineConversations: Update messages table');
|
||||
await migrateConversationMessages(obsoleteId, currentId);
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Eliminate old conversation from ConversationController lookups'
|
||||
);
|
||||
this._conversations.remove(obsolete);
|
||||
this._conversations.resetLookups();
|
||||
|
||||
log.warn('combineConversations: Complete!', {
|
||||
obsolete: obsoleteId,
|
||||
current: currentId,
|
||||
});
|
||||
}
|
||||
|
||||
// Note: we explicitly don't want to update V2 groups
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Delete the obsolete conversation from the database'
|
||||
);
|
||||
await removeConversation(obsoleteId);
|
||||
|
||||
log.warn('combineConversations: Update messages table');
|
||||
await migrateConversationMessages(obsoleteId, currentId);
|
||||
|
||||
log.warn(
|
||||
'combineConversations: Eliminate old conversation from ConversationController lookups'
|
||||
);
|
||||
this._conversations.remove(obsolete);
|
||||
this._conversations.resetLookups();
|
||||
|
||||
log.warn('combineConversations: Complete!', {
|
||||
obsolete: obsoleteId,
|
||||
current: currentId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue