Early preparations for PNP Contact Merging
This commit is contained in:
		
					parent
					
						
							
								2f5dd73e58
							
						
					
				
			
			
				commit
				
					
						faf6c41332
					
				
			
		
					 30 changed files with 1572 additions and 447 deletions
				
			
		|  | @ -1,4 +1,4 @@ | |||
| { | ||||
|   "storageProfile": "test", | ||||
|   "openDevTools": false | ||||
|   "openDevTools": true | ||||
| } | ||||
|  |  | |||
|  | @ -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.
 | ||||
|   }): 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; | ||||
|     } | ||||
| 
 | ||||
|       return newConvo.get('id'); | ||||
| 
 | ||||
|       // 2. Handle match on only E164
 | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|       return convoUuid.get('id'); | ||||
| 
 | ||||
|     // 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!'); | ||||
|       throw new Error( | ||||
|         'lookupOrCreate: convoE164 or convoUuid are falsey but should both be true!' | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // Now, we know that we have a match for both e164 and uuid checks
 | ||||
| 
 | ||||
|     // 4. If the two lookups agree, return that conversation
 | ||||
|     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'); | ||||
|       return convoUuid; | ||||
|     } | ||||
| 
 | ||||
|     // 5. If the two lookups disagree, log and return the UUID match
 | ||||
|     log.warn( | ||||
|         `ensureContactIds: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Merging.` | ||||
|       `lookupOrCreate: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Returning UUID match.` | ||||
|     ); | ||||
| 
 | ||||
|       // 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'); | ||||
|     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,8 +821,15 @@ export class ConversationController { | |||
|     current: ConversationModel, | ||||
|     obsolete: ConversationModel | ||||
|   ): Promise<void> { | ||||
|     return this._combineConversationsQueue.add(async () => { | ||||
|       const conversationType = current.get('type'); | ||||
| 
 | ||||
|       if (!this.get(obsolete.id)) { | ||||
|         log.warn( | ||||
|           `combineConversations: Already combined obsolete conversation ${obsolete.id}` | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       if (obsolete.get('type') !== conversationType) { | ||||
|         assert( | ||||
|           false, | ||||
|  | @ -654,10 +863,12 @@ export class ConversationController { | |||
|           'combineConversations: Delete all sessions tied to old conversationId' | ||||
|         ); | ||||
|         const ourUuid = window.textsecure.storage.user.getCheckedUuid(); | ||||
|       const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({ | ||||
|         const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( | ||||
|           { | ||||
|             ourUuid, | ||||
|             identifier: obsoleteUuid.toString(), | ||||
|       }); | ||||
|           } | ||||
|         ); | ||||
|         await Promise.all( | ||||
|           deviceIds.map(async deviceId => { | ||||
|             const addr = new QualifiedAddress( | ||||
|  | @ -714,6 +925,7 @@ export class ConversationController { | |||
|         obsolete: obsoleteId, | ||||
|         current: currentId, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  |  | |||
|  | @ -1045,11 +1045,11 @@ export class SignalProtocolStore extends EventsMixin { | |||
|       } | ||||
|       const { uuid, deviceId } = qualifiedAddress; | ||||
| 
 | ||||
|       const conversationId = window.ConversationController.ensureContactIds({ | ||||
|       const conversation = window.ConversationController.lookupOrCreate({ | ||||
|         uuid: uuid.toString(), | ||||
|       }); | ||||
|       strictAssert( | ||||
|         conversationId !== undefined, | ||||
|         conversation !== undefined, | ||||
|         'storeSession: Ensure contact ids failed' | ||||
|       ); | ||||
|       const id = qualifiedAddress.toString(); | ||||
|  | @ -1059,7 +1059,7 @@ export class SignalProtocolStore extends EventsMixin { | |||
|           id, | ||||
|           version: 2, | ||||
|           ourUuid: qualifiedAddress.ourUuid.toString(), | ||||
|           conversationId, | ||||
|           conversationId: conversation.id, | ||||
|           uuid: uuid.toString(), | ||||
|           deviceId, | ||||
|           record: record.serialize().toString('base64'), | ||||
|  | @ -1376,12 +1376,9 @@ export class SignalProtocolStore extends EventsMixin { | |||
|       const { uuid } = qualifiedAddress; | ||||
| 
 | ||||
|       // First, fetch this conversation
 | ||||
|       const conversationId = window.ConversationController.ensureContactIds({ | ||||
|       const conversation = window.ConversationController.lookupOrCreate({ | ||||
|         uuid: uuid.toString(), | ||||
|       }); | ||||
|       assert(conversationId, `lightSessionReset/${id}: missing conversationId`); | ||||
| 
 | ||||
|       const conversation = window.ConversationController.get(conversationId); | ||||
|       assert(conversation, `lightSessionReset/${id}: missing conversation`); | ||||
| 
 | ||||
|       log.warn(`lightSessionReset/${id}: Resetting session`); | ||||
|  |  | |||
							
								
								
									
										106
									
								
								ts/background.ts
									
										
									
									
									
								
							
							
						
						
									
										106
									
								
								ts/background.ts
									
										
									
									
									
								
							|  | @ -2593,12 +2593,13 @@ export async function startApp(): Promise<void> { | |||
| 
 | ||||
|     let conversation; | ||||
| 
 | ||||
|     const senderId = window.ConversationController.ensureContactIds({ | ||||
|     const senderConversation = window.ConversationController.maybeMergeContacts( | ||||
|       { | ||||
|         e164: sender, | ||||
|       uuid: senderUuid, | ||||
|       highTrust: true, | ||||
|         aci: senderUuid, | ||||
|         reason: `onTyping(${typing.timestamp})`, | ||||
|     }); | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     // We multiplex between GV1/GV2 groups here, but we don't kick off migrations
 | ||||
|     if (groupV2Id) { | ||||
|  | @ -2607,14 +2608,14 @@ export async function startApp(): Promise<void> { | |||
|     if (!conversation && groupId) { | ||||
|       conversation = window.ConversationController.get(groupId); | ||||
|     } | ||||
|     if (!groupV2Id && !groupId && senderId) { | ||||
|       conversation = window.ConversationController.get(senderId); | ||||
|     if (!groupV2Id && !groupId && senderConversation) { | ||||
|       conversation = senderConversation; | ||||
|     } | ||||
| 
 | ||||
|     const ourId = window.ConversationController.getOurConversationId(); | ||||
| 
 | ||||
|     if (!senderId) { | ||||
|       log.warn('onTyping: ensureContactIds returned falsey senderId!'); | ||||
|     if (!senderConversation) { | ||||
|       log.warn('onTyping: maybeMergeContacts returned falsey sender!'); | ||||
|       return; | ||||
|     } | ||||
|     if (!ourId) { | ||||
|  | @ -2648,7 +2649,6 @@ export async function startApp(): Promise<void> { | |||
|       ); | ||||
|       return; | ||||
|     } | ||||
|     const senderConversation = window.ConversationController.get(senderId); | ||||
|     if (!senderConversation) { | ||||
|       log.warn('onTyping: No conversation for sender!'); | ||||
|       return; | ||||
|  | @ -2660,6 +2660,7 @@ export async function startApp(): Promise<void> { | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const senderId = senderConversation.id; | ||||
|     conversation.notifyTyping({ | ||||
|       isTyping: started, | ||||
|       fromMe: senderId === ourId, | ||||
|  | @ -2728,14 +2729,12 @@ export async function startApp(): Promise<void> { | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const detailsId = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.maybeMergeContacts({ | ||||
|       e164: details.number, | ||||
|       uuid: details.uuid, | ||||
|       highTrust: true, | ||||
|       aci: details.uuid, | ||||
|       reason: 'onContactReceived', | ||||
|     }); | ||||
|     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|     const conversation = window.ConversationController.get(detailsId)!; | ||||
|     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
 | ||||
|  | @ -2918,10 +2917,9 @@ export async function startApp(): Promise<void> { | |||
|   function onEnvelopeReceived({ envelope }: EnvelopeEvent) { | ||||
|     const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); | ||||
|     if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { | ||||
|       window.ConversationController.ensureContactIds({ | ||||
|       window.ConversationController.maybeMergeContacts({ | ||||
|         e164: envelope.source, | ||||
|         uuid: envelope.sourceUuid, | ||||
|         highTrust: true, | ||||
|         aci: envelope.sourceUuid, | ||||
|         reason: `onEnvelopeReceived(${envelope.timestamp})`, | ||||
|       }); | ||||
|     } | ||||
|  | @ -2991,11 +2989,11 @@ export async function startApp(): Promise<void> { | |||
|         reaction.targetTimestamp, | ||||
|         'Reaction without targetTimestamp' | ||||
|       ); | ||||
|       const fromId = window.ConversationController.ensureContactIds({ | ||||
|       const fromConversation = window.ConversationController.lookupOrCreate({ | ||||
|         e164: data.source, | ||||
|         uuid: data.sourceUuid, | ||||
|       }); | ||||
|       strictAssert(fromId, 'Reaction without fromId'); | ||||
|       strictAssert(fromConversation, 'Reaction without fromConversation'); | ||||
| 
 | ||||
|       log.info('Queuing incoming reaction for', reaction.targetTimestamp); | ||||
|       const attributes: ReactionAttributesType = { | ||||
|  | @ -3004,7 +3002,7 @@ export async function startApp(): Promise<void> { | |||
|         targetAuthorUuid, | ||||
|         targetTimestamp: reaction.targetTimestamp, | ||||
|         timestamp, | ||||
|         fromId, | ||||
|         fromId: fromConversation.id, | ||||
|         source: ReactionSource.FromSomeoneElse, | ||||
|       }; | ||||
|       const reactionModel = Reactions.getSingleton().add(attributes); | ||||
|  | @ -3024,16 +3022,16 @@ export async function startApp(): Promise<void> { | |||
|         'Delete missing targetSentTimestamp' | ||||
|       ); | ||||
|       strictAssert(data.serverTimestamp, 'Delete missing serverTimestamp'); | ||||
|       const fromId = window.ConversationController.ensureContactIds({ | ||||
|       const fromConversation = window.ConversationController.lookupOrCreate({ | ||||
|         e164: data.source, | ||||
|         uuid: data.sourceUuid, | ||||
|       }); | ||||
|       strictAssert(fromId, 'Delete missing fromId'); | ||||
|       strictAssert(fromConversation, 'Delete missing fromConversation'); | ||||
| 
 | ||||
|       const attributes: DeleteAttributesType = { | ||||
|         targetSentTimestamp: del.targetSentTimestamp, | ||||
|         serverTimestamp: data.serverTimestamp, | ||||
|         fromId, | ||||
|         fromId: fromConversation.id, | ||||
|       }; | ||||
|       const deleteModel = Deletes.getSingleton().add(attributes); | ||||
| 
 | ||||
|  | @ -3056,13 +3054,11 @@ export async function startApp(): Promise<void> { | |||
|   } | ||||
| 
 | ||||
|   async function onProfileKeyUpdate({ data, confirm }: ProfileKeyUpdateEvent) { | ||||
|     const conversationId = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.maybeMergeContacts({ | ||||
|       aci: data.sourceUuid, | ||||
|       e164: data.source, | ||||
|       uuid: data.sourceUuid, | ||||
|       highTrust: true, | ||||
|       reason: 'onProfileKeyUpdate', | ||||
|     }); | ||||
|     const conversation = window.ConversationController.get(conversationId); | ||||
| 
 | ||||
|     if (!conversation) { | ||||
|       log.error( | ||||
|  | @ -3141,19 +3137,17 @@ export async function startApp(): Promise<void> { | |||
|           result: SendStateByConversationId, | ||||
|           { destinationUuid, destination, isAllowedToReplyToStory } | ||||
|         ) => { | ||||
|           const conversationId = window.ConversationController.ensureContactIds( | ||||
|             { | ||||
|           const conversation = window.ConversationController.lookupOrCreate({ | ||||
|             uuid: destinationUuid, | ||||
|             e164: destination, | ||||
|             } | ||||
|           ); | ||||
|           if (!conversationId || conversationId === ourId) { | ||||
|           }); | ||||
|           if (!conversation || conversation.id === ourId) { | ||||
|             return result; | ||||
|           } | ||||
| 
 | ||||
|           return { | ||||
|             ...result, | ||||
|             [conversationId]: { | ||||
|             [conversation.id]: { | ||||
|               isAllowedToReplyToStory, | ||||
|               status: SendStatus.Sent, | ||||
|               updatedAt: timestamp, | ||||
|  | @ -3283,15 +3277,14 @@ export async function startApp(): Promise<void> { | |||
|       } | ||||
| 
 | ||||
|       // If we can't find one, we treat this as a normal GroupV1 group
 | ||||
|       const fromContactId = window.ConversationController.ensureContactIds({ | ||||
|       const fromContact = window.ConversationController.maybeMergeContacts({ | ||||
|         aci: sourceUuid, | ||||
|         e164: source, | ||||
|         uuid: sourceUuid, | ||||
|         highTrust: true, | ||||
|         reason: `getMessageDescriptor(${message.timestamp}): group v1`, | ||||
|       }); | ||||
| 
 | ||||
|       const conversationId = window.ConversationController.ensureGroup(id, { | ||||
|         addedBy: fromContactId, | ||||
|         addedBy: fromContact?.id, | ||||
|       }); | ||||
| 
 | ||||
|       return { | ||||
|  | @ -3300,22 +3293,21 @@ export async function startApp(): Promise<void> { | |||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     const id = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.maybeMergeContacts({ | ||||
|       aci: destinationUuid, | ||||
|       e164: destination, | ||||
|       uuid: destinationUuid, | ||||
|       highTrust: true, | ||||
|       reason: `getMessageDescriptor(${message.timestamp}): private`, | ||||
|     }); | ||||
|     if (!id) { | ||||
|     if (!conversation) { | ||||
|       confirm(); | ||||
|       throw new Error( | ||||
|         `getMessageDescriptor/${message.timestamp}: ensureContactIds returned falsey id` | ||||
|         `getMessageDescriptor/${message.timestamp}: maybeMergeContacts returned falsey conversation` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       type: Message.PRIVATE, | ||||
|       id, | ||||
|       id: conversation.id, | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|  | @ -3726,11 +3718,10 @@ export async function startApp(): Promise<void> { | |||
|   }>): void { | ||||
|     const { envelopeTimestamp, timestamp, source, sourceUuid, sourceDevice } = | ||||
|       event.receipt; | ||||
|     const sourceConversationId = window.ConversationController.ensureContactIds( | ||||
|     const sourceConversation = window.ConversationController.maybeMergeContacts( | ||||
|       { | ||||
|         aci: sourceUuid, | ||||
|         e164: source, | ||||
|         uuid: sourceUuid, | ||||
|         highTrust: true, | ||||
|         reason: `onReadOrViewReceipt(${envelopeTimestamp})`, | ||||
|       } | ||||
|     ); | ||||
|  | @ -3740,14 +3731,14 @@ export async function startApp(): Promise<void> { | |||
|       sourceUuid, | ||||
|       sourceDevice, | ||||
|       envelopeTimestamp, | ||||
|       sourceConversationId, | ||||
|       sourceConversation?.id, | ||||
|       'for sent message', | ||||
|       timestamp | ||||
|     ); | ||||
| 
 | ||||
|     event.confirm(); | ||||
| 
 | ||||
|     if (!window.storage.get('read-receipt-setting') || !sourceConversationId) { | ||||
|     if (!window.storage.get('read-receipt-setting') || !sourceConversation) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|  | @ -3760,7 +3751,7 @@ export async function startApp(): Promise<void> { | |||
|     const attributes: MessageReceiptAttributesType = { | ||||
|       messageSentAt: timestamp, | ||||
|       receiptTimestamp: envelopeTimestamp, | ||||
|       sourceConversationId, | ||||
|       sourceConversationId: sourceConversation?.id, | ||||
|       sourceUuid, | ||||
|       sourceDevice, | ||||
|       type, | ||||
|  | @ -3774,10 +3765,11 @@ export async function startApp(): Promise<void> { | |||
|   function onReadSync(ev: ReadSyncEvent) { | ||||
|     const { envelopeTimestamp, sender, senderUuid, timestamp } = ev.read; | ||||
|     const readAt = envelopeTimestamp; | ||||
|     const senderId = window.ConversationController.ensureContactIds({ | ||||
|     const senderConversation = window.ConversationController.lookupOrCreate({ | ||||
|       e164: sender, | ||||
|       uuid: senderUuid, | ||||
|     }); | ||||
|     const senderId = senderConversation?.id; | ||||
| 
 | ||||
|     log.info( | ||||
|       'read sync', | ||||
|  | @ -3811,10 +3803,11 @@ export async function startApp(): Promise<void> { | |||
| 
 | ||||
|   function onViewSync(ev: ViewSyncEvent) { | ||||
|     const { envelopeTimestamp, senderE164, senderUuid, timestamp } = ev.view; | ||||
|     const senderId = window.ConversationController.ensureContactIds({ | ||||
|     const senderConversation = window.ConversationController.lookupOrCreate({ | ||||
|       e164: senderE164, | ||||
|       uuid: senderUuid, | ||||
|     }); | ||||
|     const senderId = senderConversation?.id; | ||||
| 
 | ||||
|     log.info( | ||||
|       'view sync', | ||||
|  | @ -3853,11 +3846,10 @@ export async function startApp(): Promise<void> { | |||
| 
 | ||||
|     ev.confirm(); | ||||
| 
 | ||||
|     const sourceConversationId = window.ConversationController.ensureContactIds( | ||||
|     const sourceConversation = window.ConversationController.maybeMergeContacts( | ||||
|       { | ||||
|         aci: sourceUuid, | ||||
|         e164: source, | ||||
|         uuid: sourceUuid, | ||||
|         highTrust: true, | ||||
|         reason: `onDeliveryReceipt(${envelopeTimestamp})`, | ||||
|       } | ||||
|     ); | ||||
|  | @ -3867,13 +3859,13 @@ export async function startApp(): Promise<void> { | |||
|       source, | ||||
|       sourceUuid, | ||||
|       sourceDevice, | ||||
|       sourceConversationId, | ||||
|       sourceConversation?.id, | ||||
|       envelopeTimestamp, | ||||
|       'for sent message', | ||||
|       timestamp | ||||
|     ); | ||||
| 
 | ||||
|     if (!sourceConversationId) { | ||||
|     if (!sourceConversation) { | ||||
|       log.info('no conversation for', source, sourceUuid); | ||||
|       return; | ||||
|     } | ||||
|  | @ -3891,7 +3883,7 @@ export async function startApp(): Promise<void> { | |||
|     const attributes: MessageReceiptAttributesType = { | ||||
|       messageSentAt: timestamp, | ||||
|       receiptTimestamp: envelopeTimestamp, | ||||
|       sourceConversationId, | ||||
|       sourceConversationId: sourceConversation?.id, | ||||
|       sourceUuid, | ||||
|       sourceDevice, | ||||
|       type: MessageReceiptType.Delivery, | ||||
|  |  | |||
|  | @ -2501,11 +2501,11 @@ export function buildMigrationBubble( | |||
|     ...(newAttributes.membersV2 || []).map(item => item.uuid), | ||||
|     ...(newAttributes.pendingMembersV2 || []).map(item => item.uuid), | ||||
|   ].map(uuid => { | ||||
|     const conversationId = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.lookupOrCreate({ | ||||
|       uuid, | ||||
|     }); | ||||
|     strictAssert(conversationId, `Conversation not found for ${uuid}`); | ||||
|     return conversationId; | ||||
|     strictAssert(conversation, `Conversation not found for ${uuid}`); | ||||
|     return conversation.id; | ||||
|   }); | ||||
|   const droppedMemberIds: Array<string> = difference( | ||||
|     previousGroupV1MembersIds, | ||||
|  |  | |||
|  | @ -103,12 +103,10 @@ export class MessageRequests extends Collection<MessageRequestModel> { | |||
|         conversation = window.ConversationController.get(groupId); | ||||
|       } | ||||
|       if (!conversation && (threadE164 || threadUuid)) { | ||||
|         conversation = window.ConversationController.get( | ||||
|           window.ConversationController.ensureContactIds({ | ||||
|         conversation = window.ConversationController.lookupOrCreate({ | ||||
|           e164: threadE164, | ||||
|           uuid: threadUuid, | ||||
|           }) | ||||
|         ); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (!conversation) { | ||||
|  |  | |||
|  | @ -44,11 +44,11 @@ export class Reactions extends Collection<ReactionModel> { | |||
|     const senderId = getContactId(message.attributes); | ||||
|     const sentAt = message.get('sent_at'); | ||||
|     const reactionsBySource = this.filter(re => { | ||||
|       const targetSenderId = window.ConversationController.ensureContactIds({ | ||||
|       const targetSender = window.ConversationController.lookupOrCreate({ | ||||
|         uuid: re.get('targetAuthorUuid'), | ||||
|       }); | ||||
|       const targetTimestamp = re.get('targetTimestamp'); | ||||
|       return targetSenderId === senderId && targetTimestamp === sentAt; | ||||
|       return targetSender?.id === senderId && targetTimestamp === sentAt; | ||||
|     }); | ||||
| 
 | ||||
|     if (reactionsBySource.length > 0) { | ||||
|  | @ -87,13 +87,14 @@ export class Reactions extends Collection<ReactionModel> { | |||
|     try { | ||||
|       // The conversation the target message was in; we have to find it in the database
 | ||||
|       //   to to figure that out.
 | ||||
|       const targetConversationId = | ||||
|         window.ConversationController.ensureContactIds({ | ||||
|       const targetAuthorConversation = | ||||
|         window.ConversationController.lookupOrCreate({ | ||||
|           uuid: reaction.get('targetAuthorUuid'), | ||||
|         }); | ||||
|       const targetConversationId = targetAuthorConversation?.id; | ||||
|       if (!targetConversationId) { | ||||
|         throw new Error( | ||||
|           'onReaction: No conversationId returned from ensureContactIds!' | ||||
|           'onReaction: No conversationId returned from lookupOrCreate!' | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,13 +58,13 @@ export class ReadSyncs extends Collection { | |||
|   } | ||||
| 
 | ||||
|   forMessage(message: MessageModel): ReadSyncModel | null { | ||||
|     const senderId = window.ConversationController.ensureContactIds({ | ||||
|     const sender = window.ConversationController.lookupOrCreate({ | ||||
|       e164: message.get('source'), | ||||
|       uuid: message.get('sourceUuid'), | ||||
|     }); | ||||
|     const sync = this.find(item => { | ||||
|       return ( | ||||
|         item.get('senderId') === senderId && | ||||
|         item.get('senderId') === sender?.id && | ||||
|         item.get('timestamp') === message.get('sent_at') | ||||
|       ); | ||||
|     }); | ||||
|  | @ -84,12 +84,12 @@ export class ReadSyncs extends Collection { | |||
|       ); | ||||
| 
 | ||||
|       const found = messages.find(item => { | ||||
|         const senderId = window.ConversationController.ensureContactIds({ | ||||
|         const sender = window.ConversationController.lookupOrCreate({ | ||||
|           e164: item.source, | ||||
|           uuid: item.sourceUuid, | ||||
|         }); | ||||
| 
 | ||||
|         return isIncoming(item) && senderId === sync.get('senderId'); | ||||
|         return isIncoming(item) && sender?.id === sync.get('senderId'); | ||||
|       }); | ||||
| 
 | ||||
|       if (!found) { | ||||
|  |  | |||
|  | @ -35,13 +35,13 @@ export class ViewSyncs extends Collection { | |||
|   } | ||||
| 
 | ||||
|   forMessage(message: MessageModel): Array<ViewSyncModel> { | ||||
|     const senderId = window.ConversationController.ensureContactIds({ | ||||
|     const sender = window.ConversationController.lookupOrCreate({ | ||||
|       e164: message.get('source'), | ||||
|       uuid: message.get('sourceUuid'), | ||||
|     }); | ||||
|     const syncs = this.filter(item => { | ||||
|       return ( | ||||
|         item.get('senderId') === senderId && | ||||
|         item.get('senderId') === sender?.id && | ||||
|         item.get('timestamp') === message.get('sent_at') | ||||
|       ); | ||||
|     }); | ||||
|  | @ -63,12 +63,12 @@ export class ViewSyncs extends Collection { | |||
|       ); | ||||
| 
 | ||||
|       const found = messages.find(item => { | ||||
|         const senderId = window.ConversationController.ensureContactIds({ | ||||
|         const sender = window.ConversationController.lookupOrCreate({ | ||||
|           e164: item.source, | ||||
|           uuid: item.sourceUuid, | ||||
|         }); | ||||
| 
 | ||||
|         return senderId === sync.get('senderId'); | ||||
|         return sender?.id === sync.get('senderId'); | ||||
|       }); | ||||
| 
 | ||||
|       if (!found) { | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ export function isQuoteAMatch( | |||
|   } | ||||
| 
 | ||||
|   const { authorUuid, id } = quote; | ||||
|   const authorConversationId = window.ConversationController.ensureContactIds({ | ||||
|   const authorConversation = window.ConversationController.lookupOrCreate({ | ||||
|     e164: 'author' in quote ? quote.author : undefined, | ||||
|     uuid: authorUuid, | ||||
|   }); | ||||
|  | @ -44,7 +44,7 @@ export function isQuoteAMatch( | |||
|   return ( | ||||
|     message.sent_at === id && | ||||
|     message.conversationId === conversationId && | ||||
|     getContactId(message) === authorConversationId | ||||
|     getContactId(message) === authorConversation?.id | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
|  | @ -58,10 +58,11 @@ export function getContactId( | |||
|     return window.ConversationController.getOurConversationId(); | ||||
|   } | ||||
| 
 | ||||
|   return window.ConversationController.ensureContactIds({ | ||||
|   const conversation = window.ConversationController.lookupOrCreate({ | ||||
|     e164: source, | ||||
|     uuid: sourceUuid, | ||||
|   }); | ||||
|   return conversation?.id; | ||||
| } | ||||
| 
 | ||||
| export function getContact( | ||||
|  |  | |||
							
								
								
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -322,6 +322,7 @@ export type ConversationAttributesType = { | |||
| 
 | ||||
|   // Private core info
 | ||||
|   uuid?: UUIDStringType; | ||||
|   pni?: UUIDStringType; | ||||
|   e164?: string; | ||||
| 
 | ||||
|   // Private other fields
 | ||||
|  |  | |||
|  | @ -1275,11 +1275,11 @@ export class ConversationModel extends window.Backbone | |||
|     const e164 = message.get('source'); | ||||
|     const sourceDevice = message.get('sourceDevice'); | ||||
| 
 | ||||
|     const sourceId = window.ConversationController.ensureContactIds({ | ||||
|     const source = window.ConversationController.lookupOrCreate({ | ||||
|       uuid, | ||||
|       e164, | ||||
|     }); | ||||
|     const typingToken = `${sourceId}.${sourceDevice}`; | ||||
|     const typingToken = `${source?.id}.${sourceDevice}`; | ||||
| 
 | ||||
|     // Clear typing indicator for a given contact if we receive a message from them
 | ||||
|     this.clearContactTypingTimer(typingToken); | ||||
|  | @ -1869,10 +1869,10 @@ export class ConversationModel extends window.Backbone | |||
| 
 | ||||
|   updateE164(e164?: string | null): void { | ||||
|     const oldValue = this.get('e164'); | ||||
|     if (e164 && e164 !== oldValue) { | ||||
|       this.set('e164', e164); | ||||
|     if (e164 !== oldValue) { | ||||
|       this.set('e164', e164 || undefined); | ||||
| 
 | ||||
|       if (oldValue) { | ||||
|       if (oldValue && e164) { | ||||
|         this.addChangeNumberNotification(oldValue, e164); | ||||
|       } | ||||
| 
 | ||||
|  | @ -1883,13 +1883,32 @@ export class ConversationModel extends window.Backbone | |||
| 
 | ||||
|   updateUuid(uuid?: string): void { | ||||
|     const oldValue = this.get('uuid'); | ||||
|     if (uuid && uuid !== oldValue) { | ||||
|       this.set('uuid', UUID.cast(uuid.toLowerCase())); | ||||
|     if (uuid !== oldValue) { | ||||
|       this.set('uuid', uuid ? UUID.cast(uuid.toLowerCase()) : undefined); | ||||
|       window.Signal.Data.updateConversation(this.attributes); | ||||
|       this.trigger('idUpdated', this, 'uuid', oldValue); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   updatePni(pni?: string): void { | ||||
|     const oldValue = this.get('pni'); | ||||
|     if (pni !== oldValue) { | ||||
|       this.set('pni', pni ? UUID.cast(pni.toLowerCase()) : undefined); | ||||
| 
 | ||||
|       if ( | ||||
|         oldValue && | ||||
|         pni && | ||||
|         (!this.get('uuid') || this.get('uuid') === oldValue) | ||||
|       ) { | ||||
|         // TODO: DESKTOP-3974
 | ||||
|         this.addKeyChange(UUID.checkedLookup(oldValue)); | ||||
|       } | ||||
| 
 | ||||
|       window.Signal.Data.updateConversation(this.attributes); | ||||
|       this.trigger('idUpdated', this, 'pni', oldValue); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   updateGroupId(groupId?: string): void { | ||||
|     const oldValue = this.get('groupId'); | ||||
|     if (groupId && groupId !== oldValue) { | ||||
|  | @ -5389,6 +5408,9 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({ | |||
|           if (idProp === 'uuid') { | ||||
|             delete this._byUuid[oldValue]; | ||||
|           } | ||||
|           if (idProp === 'pni') { | ||||
|             delete this._byPni[oldValue]; | ||||
|           } | ||||
|           if (idProp === 'groupId') { | ||||
|             delete this._byGroupId[oldValue]; | ||||
|           } | ||||
|  | @ -5401,6 +5423,10 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({ | |||
|         if (uuid) { | ||||
|           this._byUuid[uuid] = model; | ||||
|         } | ||||
|         const pni = model.get('pni'); | ||||
|         if (pni) { | ||||
|           this._byPni[pni] = model; | ||||
|         } | ||||
|         const groupId = model.get('groupId'); | ||||
|         if (groupId) { | ||||
|           this._byGroupId[groupId] = model; | ||||
|  | @ -5441,6 +5467,16 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({ | |||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const pni = model.get('pni'); | ||||
|       if (pni) { | ||||
|         const existing = this._byPni[pni]; | ||||
| 
 | ||||
|         // Prefer the contact with both uuid and pni
 | ||||
|         if (!existing || (existing && !existing.get('uuid'))) { | ||||
|           this._byPni[pni] = model; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const groupId = model.get('groupId'); | ||||
|       if (groupId) { | ||||
|         this._byGroupId[groupId] = model; | ||||
|  | @ -5451,6 +5487,7 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({ | |||
|   eraseLookups() { | ||||
|     this._byE164 = Object.create(null); | ||||
|     this._byUuid = Object.create(null); | ||||
|     this._byPni = Object.create(null); | ||||
|     this._byGroupId = Object.create(null); | ||||
|   }, | ||||
| 
 | ||||
|  | @ -5510,6 +5547,7 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({ | |||
|       this._byE164[id] || | ||||
|       this._byE164[`+${id}`] || | ||||
|       this._byUuid[id] || | ||||
|       this._byPni[id] || | ||||
|       this._byGroupId[id] || | ||||
|       window.Backbone.Collection.prototype.get.call(this, id) | ||||
|     ); | ||||
|  |  | |||
|  | @ -291,12 +291,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|     const sourceDevice = this.get('sourceDevice'); | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|     const sourceId = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.lookupOrCreate({ | ||||
|       e164: source, | ||||
|       uuid: sourceUuid, | ||||
|     })!; | ||||
| 
 | ||||
|     return `${sourceId}.${sourceDevice}-${sentAt}`; | ||||
|     return `${conversation?.id}.${sourceDevice}-${sentAt}`; | ||||
|   } | ||||
| 
 | ||||
|   getReceivedAt(): number { | ||||
|  | @ -2137,14 +2137,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|                 return; | ||||
|               } | ||||
| 
 | ||||
|               const destinationConversationId = | ||||
|                 window.ConversationController.ensureContactIds({ | ||||
|                   uuid: destinationUuid, | ||||
|                   e164: destination, | ||||
|                   highTrust: true, | ||||
|               const destinationConversation = | ||||
|                 window.ConversationController.maybeMergeContacts({ | ||||
|                   aci: destinationUuid, | ||||
|                   e164: destination || undefined, | ||||
|                   reason: `handleDataMessage(${initialMessage.timestamp})`, | ||||
|                 }); | ||||
|               if (!destinationConversationId) { | ||||
|               if (!destinationConversation) { | ||||
|                 return; | ||||
|               } | ||||
| 
 | ||||
|  | @ -2155,9 +2154,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
| 
 | ||||
|               const previousSendState = getOwn( | ||||
|                 sendStateByConversationId, | ||||
|                 destinationConversationId | ||||
|                 destinationConversation.id | ||||
|               ); | ||||
|               sendStateByConversationId[destinationConversationId] = | ||||
|               sendStateByConversationId[destinationConversation.id] = | ||||
|                 previousSendState | ||||
|                   ? sendStateReducer(previousSendState, { | ||||
|                       type: SendActionType.Sent, | ||||
|  | @ -2274,7 +2273,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|         UUIDKind.ACI | ||||
|       ); | ||||
|       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|       const senderId = window.ConversationController.ensureContactIds({ | ||||
|       const sender = window.ConversationController.lookupOrCreate({ | ||||
|         e164: source, | ||||
|         uuid: sourceUuid, | ||||
|       })!; | ||||
|  | @ -2348,7 +2347,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|       // Drop incoming messages to announcement only groups where sender is not admin
 | ||||
|       if ( | ||||
|         conversation.get('announcementsOnly') && | ||||
|         !conversation.isAdmin(UUID.checkedLookup(senderId)) | ||||
|         !conversation.isAdmin(UUID.checkedLookup(sender?.id)) | ||||
|       ) { | ||||
|         confirm(); | ||||
|         return; | ||||
|  | @ -2565,8 +2564,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|                 conversation.set({ addedBy: getContactId(message.attributes) }); | ||||
|               } | ||||
|             } else if (initialMessage.group.type === GROUP_TYPES.QUIT) { | ||||
|               // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|               const sender = window.ConversationController.get(senderId)!; | ||||
|               const inGroup = Boolean( | ||||
|                 sender && | ||||
|                   (conversation.get('members') || []).includes(sender.id) | ||||
|  | @ -2682,14 +2679,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> { | |||
|             } else if (isDirectConversation(conversation.attributes)) { | ||||
|               conversation.setProfileKey(profileKey); | ||||
|             } else { | ||||
|               const localId = window.ConversationController.ensureContactIds({ | ||||
|               const local = window.ConversationController.lookupOrCreate({ | ||||
|                 e164: source, | ||||
|                 uuid: sourceUuid, | ||||
|               }); | ||||
|               // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|               window.ConversationController.get(localId)!.setProfileKey( | ||||
|                 profileKey | ||||
|               ); | ||||
|               local?.setProfileKey(profileKey); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|  |  | |||
|  | @ -863,22 +863,16 @@ export async function mergeContactRecord( | |||
|     return { hasConflict: false, shouldDrop: true, details: ['our own uuid'] }; | ||||
|   } | ||||
| 
 | ||||
|   const id = window.ConversationController.ensureContactIds({ | ||||
|   const conversation = window.ConversationController.maybeMergeContacts({ | ||||
|     aci: uuid, | ||||
|     e164, | ||||
|     uuid, | ||||
|     highTrust: true, | ||||
|     reason: 'mergeContactRecord', | ||||
|   }); | ||||
| 
 | ||||
|   if (!id) { | ||||
|     throw new Error(`No ID for ${storageID}`); | ||||
|   if (!conversation) { | ||||
|     throw new Error(`No conversation for ${storageID}`); | ||||
|   } | ||||
| 
 | ||||
|   const conversation = await window.ConversationController.getOrCreateAndWait( | ||||
|     id, | ||||
|     'private' | ||||
|   ); | ||||
| 
 | ||||
|   let needsProfileFetch = false; | ||||
|   if (contactRecord.profileKey && contactRecord.profileKey.length > 0) { | ||||
|     needsProfileFetch = await conversation.setProfileKey( | ||||
|  | @ -1129,32 +1123,32 @@ export async function mergeAccountRecord( | |||
| 
 | ||||
|     const remotelyPinnedConversationPromises = pinnedConversations.map( | ||||
|       async ({ contact, legacyGroupId, groupMasterKey }) => { | ||||
|         let conversationId: string | undefined; | ||||
|         let conversation: ConversationModel | undefined; | ||||
| 
 | ||||
|         if (contact) { | ||||
|           conversationId = | ||||
|             window.ConversationController.ensureContactIds(contact); | ||||
|           conversation = window.ConversationController.lookupOrCreate(contact); | ||||
|         } else if (legacyGroupId && legacyGroupId.length) { | ||||
|           conversationId = Bytes.toBinary(legacyGroupId); | ||||
|           const groupId = Bytes.toBinary(legacyGroupId); | ||||
|           conversation = window.ConversationController.get(groupId); | ||||
|         } else if (groupMasterKey && groupMasterKey.length) { | ||||
|           const groupFields = deriveGroupFields(groupMasterKey); | ||||
|           const groupId = Bytes.toBase64(groupFields.id); | ||||
| 
 | ||||
|           conversationId = groupId; | ||||
|           conversation = window.ConversationController.get(groupId); | ||||
|         } else { | ||||
|           log.error( | ||||
|             'storageService.mergeAccountRecord: Invalid identifier received' | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         if (!conversationId) { | ||||
|         if (!conversation) { | ||||
|           log.error( | ||||
|             'storageService.mergeAccountRecord: missing conversation id.' | ||||
|           ); | ||||
|           return undefined; | ||||
|         } | ||||
| 
 | ||||
|         return window.ConversationController.get(conversationId); | ||||
|         return conversation; | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -215,6 +215,7 @@ const dataInterface: ClientInterface = { | |||
|   updateConversation, | ||||
|   updateConversations, | ||||
|   removeConversation, | ||||
|   _removeAllConversations, | ||||
|   updateAllConversationColors, | ||||
|   removeAllProfileKeyCredentials, | ||||
| 
 | ||||
|  | @ -1084,6 +1085,10 @@ async function removeConversation(id: string): Promise<void> { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| async function _removeAllConversations(): Promise<void> { | ||||
|   await channels._removeAllConversations(); | ||||
| } | ||||
| 
 | ||||
| async function eraseStorageServiceStateFromConversations(): Promise<void> { | ||||
|   await channels.eraseStorageServiceStateFromConversations(); | ||||
| } | ||||
|  |  | |||
|  | @ -414,6 +414,7 @@ export type DataInterface = { | |||
|   // updateConversation is a normal data method on Server, a sync batch-add on Client
 | ||||
|   updateConversations: (array: Array<ConversationType>) => Promise<void>; | ||||
|   // removeConversation handles either one id or an array on Server, and one id on Client
 | ||||
|   _removeAllConversations: () => Promise<void>; | ||||
|   updateAllConversationColors: ( | ||||
|     conversationColor?: ConversationColorType, | ||||
|     customColorData?: { | ||||
|  |  | |||
|  | @ -207,6 +207,7 @@ const dataInterface: ServerInterface = { | |||
|   updateConversation, | ||||
|   updateConversations, | ||||
|   removeConversation, | ||||
|   _removeAllConversations, | ||||
|   updateAllConversationColors, | ||||
|   removeAllProfileKeyCredentials, | ||||
| 
 | ||||
|  | @ -1478,6 +1479,11 @@ async function removeConversation(id: Array<string> | string): Promise<void> { | |||
|   batchMultiVarQuery(db, id, removeConversationsSync); | ||||
| } | ||||
| 
 | ||||
| async function _removeAllConversations(): Promise<void> { | ||||
|   const db = getInstance(); | ||||
|   db.prepare<EmptyQuery>('DELETE from conversations;').run(); | ||||
| } | ||||
| 
 | ||||
| async function getConversationById( | ||||
|   id: string | ||||
| ): Promise<ConversationType | undefined> { | ||||
|  |  | |||
|  | @ -121,10 +121,10 @@ const mapStateToActiveCallProp = ( | |||
|   const conversationSelectorByUuid = memoize< | ||||
|     (uuid: UUIDStringType) => undefined | ConversationType | ||||
|   >(uuid => { | ||||
|     const conversationId = window.ConversationController.ensureContactIds({ | ||||
|     const convoForUuid = window.ConversationController.lookupOrCreate({ | ||||
|       uuid, | ||||
|     }); | ||||
|     return conversationId ? conversationSelector(conversationId) : undefined; | ||||
|     return convoForUuid ? conversationSelector(convoForUuid.id) : undefined; | ||||
|   }); | ||||
| 
 | ||||
|   const baseResult = { | ||||
|  |  | |||
							
								
								
									
										865
									
								
								ts/test-electron/ConversationController_test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										865
									
								
								ts/test-electron/ConversationController_test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,865 @@ | |||
| // Copyright 2022 Signal Messenger, LLC
 | ||||
| // SPDX-License-Identifier: AGPL-3.0-only
 | ||||
| 
 | ||||
| import { assert } from 'chai'; | ||||
| 
 | ||||
| import { UUID } from '../types/UUID'; | ||||
| import { strictAssert } from '../util/assert'; | ||||
| 
 | ||||
| import type { ConversationModel } from '../models/conversations'; | ||||
| import type { UUIDStringType } from '../types/UUID'; | ||||
| 
 | ||||
| const ACI_1 = UUID.generate().toString(); | ||||
| const ACI_2 = UUID.generate().toString(); | ||||
| const E164_1 = '+14155550111'; | ||||
| const E164_2 = '+14155550112'; | ||||
| const PNI_1 = UUID.generate().toString(); | ||||
| const PNI_2 = UUID.generate().toString(); | ||||
| const reason = 'test'; | ||||
| 
 | ||||
| type ParamsType = { | ||||
|   uuid?: UUIDStringType; | ||||
|   aci?: UUIDStringType; | ||||
|   e164?: string; | ||||
|   pni?: UUIDStringType; | ||||
| }; | ||||
| 
 | ||||
| describe('ConversationController', () => { | ||||
|   describe('maybeMergeContacts', () => { | ||||
|     let mergeOldAndNew: (options: { | ||||
|       logId: string; | ||||
|       oldConversation: ConversationModel; | ||||
|       newConversation: ConversationModel; | ||||
|     }) => Promise<void>; | ||||
| 
 | ||||
|     beforeEach(async () => { | ||||
|       await window.Signal.Data._removeAllConversations(); | ||||
| 
 | ||||
|       window.ConversationController.reset(); | ||||
|       await window.ConversationController.load(); | ||||
| 
 | ||||
|       mergeOldAndNew = () => { | ||||
|         throw new Error('mergeOldAndNew: Should not be called!'); | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     // Verifying incoming data
 | ||||
|     describe('data validation', () => { | ||||
|       it('throws when provided no data', () => { | ||||
|         assert.throws(() => { | ||||
|           window.ConversationController.maybeMergeContacts({ | ||||
|             mergeOldAndNew, | ||||
|             reason, | ||||
|           }); | ||||
|         }, 'Need to provide at least one'); | ||||
|       }); | ||||
|       it('throws when provided a pni with no e164', () => { | ||||
|         assert.throws(() => { | ||||
|           window.ConversationController.maybeMergeContacts({ | ||||
|             mergeOldAndNew, | ||||
|             pni: PNI_1, | ||||
|             reason, | ||||
|           }); | ||||
|         }, 'Cannot provide pni without an e164'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     function create( | ||||
|       name: string, | ||||
|       { uuid, aci, e164, pni }: ParamsType | ||||
|     ): ConversationModel { | ||||
|       const identifier = aci || uuid || e164 || pni; | ||||
|       const serviceId = aci || uuid || pni; | ||||
| 
 | ||||
|       strictAssert(identifier, 'create needs aci, e164, pni, or uuid'); | ||||
| 
 | ||||
|       const conversation = window.ConversationController.getOrCreate( | ||||
|         identifier, | ||||
|         'private', | ||||
|         { uuid: serviceId, e164, pni } | ||||
|       ); | ||||
|       expectLookups(conversation, name, { uuid, aci, e164, pni }); | ||||
| 
 | ||||
|       return conversation; | ||||
|     } | ||||
| 
 | ||||
|     function expectLookups( | ||||
|       conversation: ConversationModel | undefined, | ||||
|       name: string, | ||||
|       { uuid, aci, e164, pni }: ParamsType | ||||
|     ) { | ||||
|       assert.exists(conversation, `${name} conversation exists`); | ||||
| 
 | ||||
|       // Verify that this conversation hasn't been deleted
 | ||||
|       assert.strictEqual( | ||||
|         window.ConversationController.get(conversation?.id)?.id, | ||||
|         conversation?.id, | ||||
|         `${name} vs. lookup by id` | ||||
|       ); | ||||
| 
 | ||||
|       if (uuid) { | ||||
|         assert.strictEqual( | ||||
|           window.ConversationController.get(uuid)?.id, | ||||
|           conversation?.id, | ||||
|           `${name} vs. lookup by uuid` | ||||
|         ); | ||||
|       } | ||||
|       if (aci) { | ||||
|         assert.strictEqual( | ||||
|           window.ConversationController.get(aci)?.id, | ||||
|           conversation?.id, | ||||
|           `${name} vs. lookup by aci` | ||||
|         ); | ||||
|       } | ||||
|       if (e164) { | ||||
|         assert.strictEqual( | ||||
|           window.ConversationController.get(e164)?.id, | ||||
|           conversation?.id, | ||||
|           `${name} vs. lookup by e164` | ||||
|         ); | ||||
|       } | ||||
|       if (pni) { | ||||
|         assert.strictEqual( | ||||
|           window.ConversationController.get(pni)?.id, | ||||
|           conversation?.id, | ||||
|           `${name} vs. lookup by pni` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     function expectPropsAndLookups( | ||||
|       conversation: ConversationModel | undefined, | ||||
|       name: string, | ||||
|       { uuid, aci, e164, pni }: ParamsType | ||||
|     ) { | ||||
|       assert.exists(conversation, `${name} conversation exists`); | ||||
|       assert.strictEqual( | ||||
|         conversation?.get('uuid'), | ||||
|         aci || uuid, | ||||
|         `${name} uuid matches` | ||||
|       ); | ||||
|       assert.strictEqual( | ||||
|         conversation?.get('e164'), | ||||
|         e164, | ||||
|         `${name} e164 matches` | ||||
|       ); | ||||
|       assert.strictEqual(conversation?.get('pni'), pni, `${name} pni matches`); | ||||
| 
 | ||||
|       expectLookups(conversation, name, { uuid, e164, pni }); | ||||
|     } | ||||
| 
 | ||||
|     function expectDeleted(conversation: ConversationModel, name: string) { | ||||
|       assert.isUndefined( | ||||
|         window.ConversationController.get(conversation.id), | ||||
|         `${name} has been deleted` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     describe('non-destructive updates', () => { | ||||
|       it('creates a new conversation with just ACI if no matches', () => { | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const second = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(second, 'second', { | ||||
|           aci: ACI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, second?.id, 'result and second match'); | ||||
|       }); | ||||
|       it('creates a new conversation with just e164 if no matches', () => { | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const second = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(second, 'second', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, second?.id, 'result and second match'); | ||||
|       }); | ||||
|       it('creates a new conversation with all data if no matches', () => { | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const second = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(second, 'second', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, second?.id, 'result and second match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('fetches all-data conversation with ACI-only query', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('fetches all-data conversation with e164+PNI query', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('adds ACI to conversation with e164+PNI', () => { | ||||
|         const initial = create('initial', { | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds e164+PNI to conversation with just ACI', () => { | ||||
|         const initial = create('initial', { | ||||
|           uuid: ACI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds e164 to conversation with ACI+PNI', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: ACI_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds PNI to conversation with ACI+e164', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds PNI to conversation with just e164', () => { | ||||
|         const initial = create('initial', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds PNI+ACI to conversation with just e164', () => { | ||||
|         const initial = create('initial', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('adds ACI+e164 to conversation with just PNI', () => { | ||||
|         const initial = create('initial', { | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('promotes PNI used as generic UUID to be in the PNI field as well', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: PNI_1, | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         expectPropsAndLookups(initial, 'initial', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         assert.strictEqual(initial?.id, result?.id, 'result and initial match'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('with destructive updates', () => { | ||||
|       it('replaces e164+PNI in conversation with matching ACI', () => { | ||||
|         const initial = create('initial', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_2, | ||||
|           pni: PNI_2, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_2, | ||||
|           pni: PNI_2, | ||||
|         }); | ||||
| 
 | ||||
|         assert.isUndefined( | ||||
|           window.ConversationController.get(E164_1), | ||||
|           'old e164 no longer found' | ||||
|         ); | ||||
|         assert.isUndefined( | ||||
|           window.ConversationController.get(PNI_1), | ||||
|           'old pni no longer found' | ||||
|         ); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('replaces PNI in conversation with e164+PNI', () => { | ||||
|         const initial = create('initial', { | ||||
|           pni: PNI_1, | ||||
|           e164: E164_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           pni: PNI_2, | ||||
|           e164: E164_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_2, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_2, | ||||
|         }); | ||||
| 
 | ||||
|         assert.isUndefined( | ||||
|           window.ConversationController.get(PNI_1), | ||||
|           'old pni no longer found' | ||||
|         ); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
|       it('replaces PNI in conversation with all data', () => { | ||||
|         const initial = create('initial', { | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_2, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_2, | ||||
|         }); | ||||
| 
 | ||||
|         assert.isUndefined( | ||||
|           window.ConversationController.get(PNI_1), | ||||
|           'old pni no longer found' | ||||
|         ); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, initial?.id, 'result and initial match'); | ||||
|       }); | ||||
| 
 | ||||
|       it('removes e164+PNI from previous conversation with an ACI, adds all data to new conversation', () => { | ||||
|         const initial = create('initial', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_2, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_2, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(initial, 'initial', { uuid: ACI_1 }); | ||||
| 
 | ||||
|         assert.notStrictEqual( | ||||
|           initial?.id, | ||||
|           result?.id, | ||||
|           'result and initial should not match' | ||||
|         ); | ||||
|       }); | ||||
|       it('removes e164+PNI from previous conversation with an ACI, adds to ACI match', () => { | ||||
|         const initial = create('initial', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
|         const aciOnly = create('aciOnly', { | ||||
|           uuid: ACI_2, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_2, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(aciOnly, 'aciOnly', { | ||||
|           uuid: ACI_2, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(initial, 'initial', { uuid: ACI_1 }); | ||||
| 
 | ||||
|         assert.strictEqual( | ||||
|           aciOnly?.id, | ||||
|           result?.id, | ||||
|           'result and aciOnly should match' | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       it('removes PNI from previous conversation, adds it to e164-only match', () => { | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           e164: E164_2, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(withPNI, 'withPNI', { e164: E164_2 }); | ||||
| 
 | ||||
|         assert.strictEqual( | ||||
|           withE164?.id, | ||||
|           result?.id, | ||||
|           'result and initial should match' | ||||
|         ); | ||||
|       }); | ||||
|       it('removes PNI from previous conversation, adds it new e164+PNI conversation', () => { | ||||
|         const initial = create('initial', { | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_2, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_2, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectPropsAndLookups(initial, 'initial', { e164: E164_1 }); | ||||
| 
 | ||||
|         assert.notStrictEqual( | ||||
|           initial?.id, | ||||
|           result?.id, | ||||
|           'result and initial should not match' | ||||
|         ); | ||||
|       }); | ||||
|       it('deletes PNI-only previous conversation, adds it to e164 match', () => { | ||||
|         mergeOldAndNew = ({ oldConversation }) => { | ||||
|           window.ConversationController.dangerouslyRemoveById( | ||||
|             oldConversation.id | ||||
|           ); | ||||
|           return Promise.resolve(); | ||||
|         }; | ||||
| 
 | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectDeleted(withPNI, 'withPNI'); | ||||
| 
 | ||||
|         assert.strictEqual( | ||||
|           withE164?.id, | ||||
|           result?.id, | ||||
|           'result and initial should match' | ||||
|         ); | ||||
|       }); | ||||
|       it('deletes previous conversation with PNI as UUID only, adds it to e164 match', () => { | ||||
|         mergeOldAndNew = ({ oldConversation }) => { | ||||
|           window.ConversationController.dangerouslyRemoveById( | ||||
|             oldConversation.id | ||||
|           ); | ||||
|           return Promise.resolve(); | ||||
|         }; | ||||
| 
 | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           uuid: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: PNI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectDeleted(withPNI, 'withPNI'); | ||||
| 
 | ||||
|         assert.strictEqual( | ||||
|           withE164?.id, | ||||
|           result?.id, | ||||
|           'result and initial should match' | ||||
|         ); | ||||
|       }); | ||||
|       it('deletes e164+PNI previous conversation, adds data to ACI match', () => { | ||||
|         mergeOldAndNew = ({ oldConversation }) => { | ||||
|           window.ConversationController.dangerouslyRemoveById( | ||||
|             oldConversation.id | ||||
|           ); | ||||
|           return Promise.resolve(); | ||||
|         }; | ||||
| 
 | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
|         const withACI = create('withPNI', { | ||||
|           aci: ACI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectDeleted(withE164, 'withE164'); | ||||
| 
 | ||||
|         assert.strictEqual( | ||||
|           withACI?.id, | ||||
|           result?.id, | ||||
|           'result and initial should match' | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       it('handles three matching conversations: ACI-only, with E164, and with PNI', () => { | ||||
|         const withACI = create('withACI', { | ||||
|           aci: ACI_1, | ||||
|         }); | ||||
|         const withE164 = create('withE164', { | ||||
|           aci: ACI_2, | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           pni: PNI_1, | ||||
|           e164: E164_2, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
|         expectPropsAndLookups(withE164, 'withE164', { aci: ACI_2 }); | ||||
|         expectPropsAndLookups(withPNI, 'withPNI', { e164: E164_2 }); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, withACI?.id, 'result and withACI match'); | ||||
|       }); | ||||
|       it('handles three matching conversations: ACI-only, E164-only (deleted), and with PNI', () => { | ||||
|         mergeOldAndNew = ({ oldConversation }) => { | ||||
|           window.ConversationController.dangerouslyRemoveById( | ||||
|             oldConversation.id | ||||
|           ); | ||||
|           return Promise.resolve(); | ||||
|         }; | ||||
| 
 | ||||
|         const withACI = create('withACI', { | ||||
|           aci: ACI_1, | ||||
|         }); | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           pni: PNI_1, | ||||
|           e164: E164_2, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
|         expectPropsAndLookups(withPNI, 'withPNI', { e164: E164_2 }); | ||||
| 
 | ||||
|         expectDeleted(withE164, 'withE164'); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, withACI?.id, 'result and withACI match'); | ||||
|       }); | ||||
|       it('merges three matching conversations: ACI-only, E164-only (deleted), PNI-only (deleted)', () => { | ||||
|         mergeOldAndNew = ({ oldConversation }) => { | ||||
|           window.ConversationController.dangerouslyRemoveById( | ||||
|             oldConversation.id | ||||
|           ); | ||||
|           return Promise.resolve(); | ||||
|         }; | ||||
| 
 | ||||
|         const withACI = create('withACI', { | ||||
|           aci: ACI_1, | ||||
|         }); | ||||
|         const withE164 = create('withE164', { | ||||
|           e164: E164_1, | ||||
|         }); | ||||
|         const withPNI = create('withPNI', { | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         const result = window.ConversationController.maybeMergeContacts({ | ||||
|           mergeOldAndNew, | ||||
|           aci: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|           reason, | ||||
|         }); | ||||
|         expectPropsAndLookups(result, 'result', { | ||||
|           uuid: ACI_1, | ||||
|           e164: E164_1, | ||||
|           pni: PNI_1, | ||||
|         }); | ||||
| 
 | ||||
|         expectDeleted(withPNI, 'withPNI'); | ||||
|         expectDeleted(withE164, 'withE164'); | ||||
| 
 | ||||
|         assert.strictEqual(result?.id, withACI?.id, 'result and withACI match'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -27,15 +27,15 @@ describe('updateConversationsWithUuidLookup', () => { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     ensureContactIds({ | ||||
|     maybeMergeContacts({ | ||||
|       e164, | ||||
|       uuid: uuidFromServer, | ||||
|       highTrust, | ||||
|       aci: uuidFromServer, | ||||
|       reason, | ||||
|     }: { | ||||
|       e164?: string | null; | ||||
|       uuid?: string | null; | ||||
|       highTrust?: boolean; | ||||
|     }): string | undefined { | ||||
|       aci?: string | null; | ||||
|       reason?: string; | ||||
|     }): ConversationModel | undefined { | ||||
|       assert( | ||||
|         e164, | ||||
|         'FakeConversationController is not set up for this case (E164 must be provided)' | ||||
|  | @ -45,8 +45,51 @@ describe('updateConversationsWithUuidLookup', () => { | |||
|         'FakeConversationController is not set up for this case (UUID must be provided)' | ||||
|       ); | ||||
|       assert( | ||||
|         highTrust, | ||||
|         'FakeConversationController is not set up for this case (must be "high trust")' | ||||
|         reason, | ||||
|         'FakeConversationController must be provided a reason when merging' | ||||
|       ); | ||||
|       const normalizedUuid = uuidFromServer!.toLowerCase(); | ||||
| 
 | ||||
|       const convoE164 = this.get(e164); | ||||
|       const convoUuid = this.get(normalizedUuid); | ||||
|       assert( | ||||
|         convoE164 || convoUuid, | ||||
|         'FakeConversationController is not set up for this case (at least one conversation should be found)' | ||||
|       ); | ||||
| 
 | ||||
|       if (convoE164 && convoUuid) { | ||||
|         if (convoE164 === convoUuid) { | ||||
|           return convoUuid; | ||||
|         } | ||||
| 
 | ||||
|         convoE164.unset('e164'); | ||||
|         convoUuid.updateE164(e164); | ||||
|         return convoUuid; | ||||
|       } | ||||
| 
 | ||||
|       if (convoE164 && !convoUuid) { | ||||
|         convoE164.updateUuid(normalizedUuid); | ||||
|         return convoE164; | ||||
|       } | ||||
| 
 | ||||
|       assert.fail('FakeConversationController should never get here'); | ||||
|       return undefined; | ||||
|     } | ||||
| 
 | ||||
|     lookupOrCreate({ | ||||
|       e164, | ||||
|       uuid: uuidFromServer, | ||||
|     }: { | ||||
|       e164?: string | null; | ||||
|       uuid?: string | null; | ||||
|     }): string | undefined { | ||||
|       assert( | ||||
|         e164, | ||||
|         'FakeConversationController is not set up for this case (E164 must be provided)' | ||||
|       ); | ||||
|       assert( | ||||
|         uuidFromServer, | ||||
|         'FakeConversationController is not set up for this case (UUID must be provided)' | ||||
|       ); | ||||
|       const normalizedUuid = uuidFromServer!.toLowerCase(); | ||||
| 
 | ||||
|  | @ -62,13 +105,10 @@ describe('updateConversationsWithUuidLookup', () => { | |||
|           return convoUuid.get('id'); | ||||
|         } | ||||
| 
 | ||||
|         convoE164.unset('e164'); | ||||
|         convoUuid.updateE164(e164); | ||||
|         return convoUuid.get('id'); | ||||
|       } | ||||
| 
 | ||||
|       if (convoE164 && !convoUuid) { | ||||
|         convoE164.updateUuid(normalizedUuid); | ||||
|         return convoE164.get('id'); | ||||
|       } | ||||
| 
 | ||||
|  | @ -218,7 +258,7 @@ describe('updateConversationsWithUuidLookup', () => { | |||
|     assert.isUndefined(conversation.get('discoveredUnregisteredAt')); | ||||
|   }); | ||||
| 
 | ||||
|   it('marks conversations unregistered if we already had a UUID for them, even if the account does not exist on server', async () => { | ||||
|   it('marks conversations unregistered and removes UUID if the account does not exist on server', async () => { | ||||
|     const existingUuid = UUID.generate().toString(); | ||||
|     const conversation = createConversation({ | ||||
|       e164: '+13215559876', | ||||
|  | @ -238,7 +278,7 @@ describe('updateConversationsWithUuidLookup', () => { | |||
|       messaging: fakeMessaging, | ||||
|     }); | ||||
| 
 | ||||
|     assert.strictEqual(conversation.get('uuid'), existingUuid); | ||||
|     assert.isUndefined(conversation.get('uuid')); | ||||
|     assert.isNumber(conversation.get('discoveredUnregisteredAt')); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -615,10 +615,9 @@ export default class AccountManager extends EventTarget { | |||
|     // This needs to be done very early, because it changes how things are saved in the
 | ||||
|     //   database. Your identity, for example, in the saveIdentityWithAttributes call
 | ||||
|     //   below.
 | ||||
|     const conversationId = window.ConversationController.ensureContactIds({ | ||||
|     const conversationId = window.ConversationController.maybeMergeContacts({ | ||||
|       aci: ourUuid, | ||||
|       e164: number, | ||||
|       uuid: ourUuid, | ||||
|       highTrust: true, | ||||
|       reason: 'createAccount', | ||||
|     }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [ | |||
|   'incoming-call-notification', | ||||
|   'notification-draw-attention', | ||||
|   'notification-setting', | ||||
|   'pinnedConversationIds', | ||||
|   'preferred-audio-input-device', | ||||
|   'preferred-audio-output-device', | ||||
|   'preferred-video-input-device', | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ export async function updateConversationsWithUuidLookup({ | |||
| }: Readonly<{ | ||||
|   conversationController: Pick< | ||||
|     ConversationController, | ||||
|     'ensureContactIds' | 'get' | ||||
|     'maybeMergeContacts' | 'get' | ||||
|   >; | ||||
|   conversations: ReadonlyArray<ConversationModel>; | ||||
|   messaging: Pick<SendMessage, 'getUuidsForE164s' | 'checkAccountExistence'>; | ||||
|  | @ -40,14 +40,12 @@ export async function updateConversationsWithUuidLookup({ | |||
| 
 | ||||
|       const uuidFromServer = getOwn(serverLookup, e164); | ||||
|       if (uuidFromServer) { | ||||
|         const finalConversationId = conversationController.ensureContactIds({ | ||||
|         const maybeFinalConversation = | ||||
|           conversationController.maybeMergeContacts({ | ||||
|             aci: uuidFromServer, | ||||
|             e164, | ||||
|           uuid: uuidFromServer, | ||||
|           highTrust: true, | ||||
|             reason: 'updateConversationsWithUuidLookup', | ||||
|           }); | ||||
|         const maybeFinalConversation = | ||||
|           conversationController.get(finalConversationId); | ||||
|         assert( | ||||
|           maybeFinalConversation, | ||||
|           'updateConversationsWithUuidLookup: expected a conversation to be found or created' | ||||
|  |  | |||
|  | @ -68,14 +68,14 @@ function isStoryAMatch( | |||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   const authorConversationId = window.ConversationController.ensureContactIds({ | ||||
|   const authorConversation = window.ConversationController.lookupOrCreate({ | ||||
|     e164: undefined, | ||||
|     uuid: authorUuid, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     message.sent_at === sentTimestamp && | ||||
|     getContactId(message) === authorConversationId && | ||||
|     getContactId(message) === authorConversation?.id && | ||||
|     (message.conversationId === conversationId || | ||||
|       message.conversationId === ourConversationId) | ||||
|   ); | ||||
|  |  | |||
|  | @ -4,15 +4,11 @@ | |||
| import * as log from '../logging/log'; | ||||
| import { profileService } from '../services/profiles'; | ||||
| 
 | ||||
| export async function getProfile( | ||||
|   providedUuid?: string, | ||||
|   providedE164?: string | ||||
| ): Promise<void> { | ||||
|   const id = window.ConversationController.ensureContactIds({ | ||||
|     uuid: providedUuid, | ||||
|     e164: providedE164, | ||||
| export async function getProfile(uuid?: string, e164?: string): Promise<void> { | ||||
|   const c = window.ConversationController.lookupOrCreate({ | ||||
|     uuid, | ||||
|     e164, | ||||
|   }); | ||||
|   const c = window.ConversationController.get(id); | ||||
|   if (!c) { | ||||
|     log.error('getProfile: failed to find conversation; doing nothing'); | ||||
|     return; | ||||
|  |  | |||
|  | @ -616,18 +616,9 @@ function startAutomaticSessionReset(decryptionError: DecryptionErrorEventData) { | |||
| 
 | ||||
|   scheduleSessionReset(senderUuid, senderDevice); | ||||
| 
 | ||||
|   const conversationId = window.ConversationController.ensureContactIds({ | ||||
|   const conversation = window.ConversationController.lookupOrCreate({ | ||||
|     uuid: senderUuid, | ||||
|   }); | ||||
| 
 | ||||
|   if (!conversationId) { | ||||
|     log.warn( | ||||
|       'onLightSessionReset: No conversation id, cannot add message to timeline' | ||||
|     ); | ||||
|     return; | ||||
|   } | ||||
|   const conversation = window.ConversationController.get(conversationId); | ||||
| 
 | ||||
|   if (!conversation) { | ||||
|     log.warn( | ||||
|       'onLightSessionReset: No conversation, cannot add message to timeline' | ||||
|  |  | |||
|  | @ -73,25 +73,24 @@ export async function lookupConversationWithoutUuid( | |||
|       const serverLookup = await messaging.getUuidsForE164s([options.e164]); | ||||
| 
 | ||||
|       if (serverLookup[options.e164]) { | ||||
|         conversationId = window.ConversationController.ensureContactIds({ | ||||
|         const convo = window.ConversationController.maybeMergeContacts({ | ||||
|           aci: serverLookup[options.e164] || undefined, | ||||
|           e164: options.e164, | ||||
|           uuid: serverLookup[options.e164], | ||||
|           highTrust: true, | ||||
|           reason: 'startNewConversationWithoutUuid(e164)', | ||||
|         }); | ||||
|         conversationId = convo?.id; | ||||
|       } | ||||
|     } else { | ||||
|       const foundUsername = await checkForUsername(options.username); | ||||
|       if (foundUsername) { | ||||
|         conversationId = window.ConversationController.ensureContactIds({ | ||||
|         const convo = window.ConversationController.lookupOrCreate({ | ||||
|           uuid: foundUsername.uuid, | ||||
|           highTrust: true, | ||||
|           reason: 'startNewConversationWithoutUuid(username)', | ||||
|         }); | ||||
| 
 | ||||
|         const convo = window.ConversationController.get(conversationId); | ||||
|         strictAssert(convo, 'We just ensured conversation existence'); | ||||
| 
 | ||||
|         conversationId = convo.id; | ||||
| 
 | ||||
|         convo.set({ username: foundUsername.username }); | ||||
|       } | ||||
|     } | ||||
|  |  | |||
|  | @ -89,10 +89,10 @@ export async function markConversationRead( | |||
|       originalReadStatus: messageSyncData.originalReadStatus, | ||||
|       senderE164: messageSyncData.source, | ||||
|       senderUuid: messageSyncData.sourceUuid, | ||||
|       senderId: window.ConversationController.ensureContactIds({ | ||||
|       senderId: window.ConversationController.lookupOrCreate({ | ||||
|         e164: messageSyncData.source, | ||||
|         uuid: messageSyncData.sourceUuid, | ||||
|       }), | ||||
|       })?.id, | ||||
|       timestamp: messageSyncData.sent_at, | ||||
|       hasErrors: message ? hasErrors(message.attributes) : false, | ||||
|     }; | ||||
|  |  | |||
|  | @ -63,21 +63,21 @@ export async function sendReceipts({ | |||
|         return result; | ||||
|       } | ||||
| 
 | ||||
|       const senderId = window.ConversationController.ensureContactIds({ | ||||
|       const sender = window.ConversationController.lookupOrCreate({ | ||||
|         e164: senderE164, | ||||
|         uuid: senderUuid, | ||||
|       }); | ||||
|       if (!senderId) { | ||||
|       if (!sender) { | ||||
|         throw new Error( | ||||
|           'no conversation found with that E164/UUID. Cannot send this receipt' | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       const existingGroup = result.get(senderId); | ||||
|       const existingGroup = result.get(sender.id); | ||||
|       if (existingGroup) { | ||||
|         existingGroup.push(receipt); | ||||
|       } else { | ||||
|         result.set(senderId, [receipt]); | ||||
|         result.set(sender.id, [receipt]); | ||||
|       } | ||||
| 
 | ||||
|       return result; | ||||
|  |  | |||
|  | @ -1354,12 +1354,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> { | |||
|                 message: { | ||||
|                   attachments: message.attachments || [], | ||||
|                   conversationId: | ||||
|                     window.ConversationController.get( | ||||
|                       window.ConversationController.ensureContactIds({ | ||||
|                     window.ConversationController.lookupOrCreate({ | ||||
|                       uuid: message.sourceUuid, | ||||
|                       e164: message.source, | ||||
|                       }) | ||||
|                     )?.id || message.conversationId, | ||||
|                     })?.id || message.conversationId, | ||||
|                   id: message.id, | ||||
|                   received_at: message.received_at, | ||||
|                   received_at_ms: Number(message.received_at_ms), | ||||
|  | @ -1816,12 +1814,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> { | |||
|           attachments: message.get('attachments') || [], | ||||
|           id: message.get('id'), | ||||
|           conversationId: | ||||
|             window.ConversationController.get( | ||||
|               window.ConversationController.ensureContactIds({ | ||||
|             window.ConversationController.lookupOrCreate({ | ||||
|               uuid: message.get('sourceUuid'), | ||||
|               e164: message.get('source'), | ||||
|               }) | ||||
|             )?.id || message.get('conversationId'), | ||||
|             })?.id || message.get('conversationId'), | ||||
|           received_at: message.get('received_at'), | ||||
|           received_at_ms: Number(message.get('received_at_ms')), | ||||
|           sent_at: message.get('sent_at'), | ||||
|  | @ -2116,16 +2112,16 @@ export class ConversationView extends window.Backbone.View<ConversationModel> { | |||
|   } | ||||
| 
 | ||||
|   startConversation(e164: string, uuid: UUIDStringType): void { | ||||
|     const conversationId = window.ConversationController.ensureContactIds({ | ||||
|     const conversation = window.ConversationController.lookupOrCreate({ | ||||
|       e164, | ||||
|       uuid, | ||||
|     }); | ||||
|     strictAssert( | ||||
|       conversationId, | ||||
|       conversation, | ||||
|       `startConversation failed given ${e164}/${uuid} combination` | ||||
|     ); | ||||
| 
 | ||||
|     this.openConversation(conversationId); | ||||
|     this.openConversation(conversation.id); | ||||
|   } | ||||
| 
 | ||||
|   async openConversation( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Scott Nonnenberg
				Scott Nonnenberg