Refactor recipient merging.

fork-5.53.8
Greyson Parrelli 2022-01-12 13:08:24 -05:00
rodzic 8aefd59eaa
commit 9ba5660f5b
2 zmienionych plików z 323 dodań i 132 usunięć

Wyświetl plik

@ -8,6 +8,9 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
@ -262,8 +265,11 @@ class RecipientDatabaseTest {
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
@ -274,6 +280,32 @@ class RecipientDatabaseTest {
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** Low trust means you cant merge. If youre retrieving a user from the table with this data, prefer the ACI one. */
@ -297,6 +329,9 @@ class RecipientDatabaseTest {
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
@ -310,6 +345,8 @@ class RecipientDatabaseTest {
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireAci())
assertFalse(existingRecipient2.hasE164())
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@ -376,6 +413,63 @@ class RecipientDatabaseTest {
assertEquals(E164_A, recipientWithId1.requireE164())
}
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfFalse() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfTrue() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Verifying a case where a change number job is expected to be enqueued. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
// ==============================================================
// Misc
// ==============================================================
@ -426,6 +520,24 @@ class RecipientDatabaseTest {
}
}
private class ChangeNumberListener {
var numberChangeWasEnqueued = false
private set
fun waitForJobManager() {
ApplicationDependencies.getJobManager().flush()
ThreadUtil.sleep(500)
}
fun enqueue() {
ApplicationDependencies.getJobManager().addListener(
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
{ _, _ -> numberChangeWasEnqueued = true }
)
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))

Wyświetl plik

@ -398,152 +398,76 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
fun getAndPossiblyMerge(aci: ACI?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientId {
require(!(aci == null && e164 == null)) { "Must provide an ACI or E164!" }
var recipientNeedingRefresh: RecipientId? = null
var remapped: Pair<RecipientId, RecipientId>? = null
var recipientChangedNumber: RecipientId? = null
var transactionSuccessful = false
val db = writableDatabase
var transactionSuccessful = false
var remapped: Pair<RecipientId, RecipientId>? = null
var recipientsNeedingRefresh: List<RecipientId> = listOf()
var recipientChangedNumber: RecipientId? = null
db.beginTransaction()
try {
val byE164 = e164?.let { getByE164(it) } ?: Optional.absent()
val byAci = aci?.let { getByAci(it) } ?: Optional.absent()
val finalId: RecipientId
val fetch: RecipientFetch = fetchRecipient(aci, e164, highTrust, changeSelf)
if (!byE164.isPresent && !byAci.isPresent) {
Log.i(TAG, "Discovered a completely new user. Inserting.", true)
finalId = if (highTrust) {
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(e164, aci))
RecipientId.from(id)
} else {
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(if (aci == null) e164 else null, aci))
if (fetch.logBundle != null) {
Log.w(TAG, fetch.toString())
}
val resolvedId: RecipientId = when (fetch) {
is RecipientFetch.Match -> {
fetch.id
}
is RecipientFetch.MatchAndUpdateE164 -> {
setPhoneNumber(fetch.id, fetch.e164)
recipientsNeedingRefresh = listOf(fetch.id)
recipientChangedNumber = fetch.changedNumber
fetch.id
}
is RecipientFetch.MatchAndReassignE164 -> {
removePhoneNumber(fetch.e164Id, db)
setPhoneNumber(fetch.id, fetch.e164)
recipientsNeedingRefresh = listOf(fetch.id, fetch.e164Id)
recipientChangedNumber = fetch.changedNumber
fetch.id
}
is RecipientFetch.MatchAndUpdateAci -> {
markRegistered(fetch.id, fetch.aci)
recipientsNeedingRefresh = listOf(fetch.id)
fetch.id
}
is RecipientFetch.MatchAndInsertAci -> {
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, fetch.aci))
RecipientId.from(id)
}
} else if (byE164.isPresent && !byAci.isPresent) {
if (aci != null) {
val e164Record: RecipientRecord = getRecord(byE164.get())
if (e164Record.aci != null) {
if (highTrust && e164Record.aci != SignalStore.account().aci) {
Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. High-trust, so stripping the E164 ($e164) from the existing account and assigning it to a new entry.", true)
removePhoneNumber(byE164.get(), db)
recipientNeedingRefresh = byE164.get()
val insertValues = buildContentValuesForNewUser(e164, aci)
insertValues.put(BLOCKED, if (e164Record.isBlocked) 1 else 0)
val id = db.insert(TABLE_NAME, null, insertValues)
finalId = RecipientId.from(id)
} else {
if (e164Record.aci == SignalStore.account().aci) {
Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user is us! Likely a case where we changed our number, but the old owner is sending sealed sender messages. Keeping the E164 ($e164) for ourselves and making a new user for the ACI.", true)
} else {
Log.w(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}), but that user already has an ACI (${e164Record.aci}). Likely a case of re-registration. Low-trust, so making a new user for the ACI.", true)
}
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci))
finalId = RecipientId.from(id)
}
} else {
finalId = if (highTrust) {
Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). High-trust, so updating.", true)
markRegisteredOrThrow(byE164.get(), aci)
byE164.get()
} else {
Log.i(TAG, "Found out about an ACI ($aci) for a known E164 user (${byE164.get()}). Low-trust, so making a new user for the ACI.", true)
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(null, aci))
RecipientId.from(id)
}
}
} else {
finalId = byE164.get()
is RecipientFetch.MatchAndMerge -> {
remapped = Pair(fetch.e164Id, fetch.aciId)
val mergedId: RecipientId = merge(fetch.aciId, fetch.e164Id)
recipientsNeedingRefresh = listOf(mergedId)
recipientChangedNumber = fetch.changedNumber
mergedId
}
} else if (!byE164.isPresent && byAci.isPresent) {
if (e164 != null) {
if (highTrust) {
if (aci == SignalStore.account().aci && !changeSelf) {
Log.w(TAG, "Found out about an E164 ($e164) for our own ACI user (${byAci.get()}). High-trust but not change self, doing nothing.", true)
finalId = byAci.get()
} else {
Log.i(TAG, "Found out about an E164 ($e164) for a known ACI user (${byAci.get()}). High-trust, so updating.", true)
val aciRecord: RecipientRecord = getRecord(byAci.get())
setPhoneNumberOrThrow(byAci.get(), e164)
finalId = byAci.get()
if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) {
recipientChangedNumber = finalId
}
}
} else {
Log.i(TAG, "Found out about an E164 ($e164) for a known ACI user (${byAci.get()}). Low-trust, so doing nothing.", true)
finalId = byAci.get()
}
} else {
finalId = byAci.get()
is RecipientFetch.Insert -> {
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(fetch.e164, fetch.aci))
RecipientId.from(id)
}
} else {
if (byE164 == byAci) {
finalId = byAci.get()
} else {
Log.w(TAG, "Hit a conflict between ${byE164.get()} (E164 of $e164) and ${byAci.get()} (ACI $aci). They map to different recipients.", Throwable(), true)
val e164Record: RecipientRecord = getRecord(byE164.get())
if (e164Record.aci != null) {
if (highTrust && e164Record.aci != SignalStore.account().aci) {
Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci). Likely a case of re-registration. High-trust, so stripping the E164 ($e164) from the existing account and assigning it to the ACI entry.", true)
removePhoneNumber(byE164.get(), db)
recipientNeedingRefresh = byE164.get()
val aciRecord: RecipientRecord = getRecord(byAci.get())
setPhoneNumberOrThrow(byAci.get(), e164!!)
finalId = byAci.get()
if (!Util.isEmpty(aciRecord.e164) && aciRecord.e164 != e164) {
recipientChangedNumber = finalId
}
} else {
if (e164Record.aci == SignalStore.account().aci) {
Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci), but the E164 contact is us! Likely a case where we changed our number, but the old owner is sending sealed sender messages. Keeping the E164 ($e164) for ourselves.", true)
} else {
Log.w(TAG, "The E164 contact (${byE164.get()}) has a different ACI ($aci). Likely a case of re-registration. Low-trust, so doing nothing.", true)
}
finalId = byAci.get()
}
} else {
val aciRecord: RecipientRecord = getRecord(byAci.get())
if (aciRecord.e164 != null) {
if (highTrust) {
Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. High-trust, so merging the two rows together. The E164 has also effectively changed for the ACI contact.", true)
finalId = merge(byAci.get(), byE164.get())
recipientNeedingRefresh = byAci.get()
remapped = Pair(byE164.get(), byAci.get())
recipientChangedNumber = finalId
} else {
Log.w(TAG, "We have one contact with just an E164, and another with both an ACI and a different E164. Low-trust, so doing nothing.", true)
finalId = byAci.get()
}
} else {
if (highTrust) {
Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. High-trust, so merging the two rows together.", true)
finalId = merge(byAci.get(), byE164.get())
recipientNeedingRefresh = byAci.get()
remapped = Pair(byE164.get(), byAci.get())
} else {
Log.w(TAG, "We have one contact with just an E164, and another with just an ACI. Low-trust, so doing nothing.", true)
finalId = byAci.get()
}
}
}
is RecipientFetch.InsertAndReassignE164 -> {
removePhoneNumber(fetch.e164Id, db)
recipientsNeedingRefresh = listOf(fetch.e164Id)
val id = db.insert(TABLE_NAME, null, buildContentValuesForNewUser(fetch.e164, fetch.aci))
RecipientId.from(id)
}
}
db.setTransactionSuccessful()
transactionSuccessful = true
return finalId
db.setTransactionSuccessful()
return resolvedId
} finally {
db.endTransaction()
if (transactionSuccessful) {
if (recipientNeedingRefresh != null) {
Recipient.live(recipientNeedingRefresh).refresh()
RetrieveProfileJob.enqueue(recipientNeedingRefresh)
if (recipientsNeedingRefresh.isNotEmpty()) {
recipientsNeedingRefresh.forEach { Recipient.live(it).refresh() }
RetrieveProfileJob.enqueue(recipientsNeedingRefresh.toSet())
}
if (remapped != null) {
@ -551,7 +475,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
ApplicationDependencies.getRecipientCache().remap(remapped.first(), remapped.second())
}
if (recipientNeedingRefresh != null || remapped != null) {
if (recipientsNeedingRefresh.isNotEmpty() || remapped != null) {
StorageSyncHelper.scheduleSyncForDataChange()
RecipientId.clearCache()
}
@ -563,6 +487,83 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
private fun fetchRecipient(aci: ACI?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientFetch {
val byE164 = e164?.let { getByE164(it) } ?: Optional.absent()
val byAci = aci?.let { getByAci(it) } ?: Optional.absent()
var logs = LogBundle(
byAci = byAci.transform { id -> RecipientLogDetails(id = id) }.orNull(),
byE164 = byE164.transform { id -> RecipientLogDetails(id = id) }.orNull(),
label = "L0"
)
if (byAci.isPresent && byE164.isPresent && byAci.get() == byE164.get()) {
return RecipientFetch.Match(byAci.get(), null)
}
if (byAci.isPresent && byE164.isAbsent()) {
val aciRecord: RecipientRecord = getRecord(byAci.get())
logs = logs.copy(byAci = aciRecord.toLogDetails())
if (highTrust && e164 != null && (changeSelf || aci != SignalStore.account().aci)) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null && aciRecord.e164 != e164) aciRecord.id else null
return RecipientFetch.MatchAndUpdateE164(byAci.get(), e164, changedNumber, logs.label("L1"))
} else if (e164 == null) {
return RecipientFetch.Match(byAci.get(), null)
} else {
return RecipientFetch.Match(byAci.get(), logs.label("L2"))
}
}
if (byAci.isAbsent() && byE164.isPresent) {
val e164Record: RecipientRecord = getRecord(byE164.get())
logs = logs.copy(byE164 = e164Record.toLogDetails())
if (highTrust && aci != null && e164Record.aci == null) {
return RecipientFetch.MatchAndUpdateAci(byE164.get(), aci, logs.label("L3"))
} else if (highTrust && aci != null && e164Record.aci != SignalStore.account().aci) {
return RecipientFetch.InsertAndReassignE164(aci, e164, byE164.get(), logs.label("L4"))
} else if (aci != null) {
return RecipientFetch.Insert(aci, null, logs.label("L5"))
} else {
return RecipientFetch.Match(byE164.get(), null)
}
}
if (byAci.isAbsent() && byE164.isAbsent()) {
if (highTrust) {
return RecipientFetch.Insert(aci, e164, logs.label("L6"))
} else if (aci != null) {
return RecipientFetch.Insert(aci, null, logs.label("L7"))
} else {
return RecipientFetch.Insert(null, e164, logs.label("L8"))
}
}
require(byAci.isPresent && byE164.isPresent && byAci.get() != byE164.get()) { "Assumed conditions at this point." }
val aciRecord: RecipientRecord = getRecord(byAci.get())
val e164Record: RecipientRecord = getRecord(byE164.get())
logs = logs.copy(byAci = aciRecord.toLogDetails(), byE164 = e164Record.toLogDetails())
if (e164Record.aci == null) {
if (highTrust) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null
return RecipientFetch.MatchAndMerge(aciId = byAci.get(), e164Id = byE164.get(), changedNumber = changedNumber, logs.label("L9"))
} else {
return RecipientFetch.Match(byAci.get(), logs.label("L10"))
}
} else {
if (highTrust && e164Record.aci != SignalStore.account().aci) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null
return RecipientFetch.MatchAndReassignE164(id = byAci.get(), e164Id = byE164.get(), e164 = e164!!, changedNumber = changedNumber, logs.label("L11"))
} else {
return RecipientFetch.Match(byAci.get(), logs.label("L12"))
}
}
}
fun getOrInsertFromAci(aci: ACI): RecipientId {
return getOrInsertByColumn(ACI_COLUMN, aci.toString()).recipientId
}
@ -2985,6 +2986,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column"
}
private fun <T> Optional<T>.isAbsent(): Boolean {
return !this.isPresent
}
private fun RecipientRecord.toLogDetails(): RecipientLogDetails {
return RecipientLogDetails(
id = this.id,
aci = this.aci,
e164 = this.e164
)
}
inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) {
private val pendingRecipients: MutableSet<RecipientId> = mutableSetOf()
@ -3272,4 +3285,70 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
}
private sealed class RecipientFetch(val logBundle: LogBundle?) {
/**
* We have a matching recipient, and no writes need to occur.
*/
data class Match(val id: RecipientId, val bundle: LogBundle?) : RecipientFetch(bundle)
/**
* We found a matching recipient and can update them with a new E164.
*/
data class MatchAndUpdateE164(val id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can give them an E164 that used to belong to someone else.
*/
data class MatchAndReassignE164(val id: RecipientId, val e164Id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can update them with a new ACI.
*/
data class MatchAndUpdateAci(val id: RecipientId, val aci: ACI, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can insert an ACI as a *new user*.
*/
data class MatchAndInsertAci(val id: RecipientId, val aci: ACI, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* The ACI maps to ACI-only recipient, and the E164 maps to a different E164-only recipient. We need to merge the two together.
*/
data class MatchAndMerge(val aciId: RecipientId, val e164Id: RecipientId, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We don't have a matching recipient, so we need to insert one.
*/
data class Insert(val aci: ACI?, val e164: String?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We need to create a new recipient and give it the E164 of an existing recipient.
*/
data class InsertAndReassignE164(val aci: ACI?, val e164: String?, val e164Id: RecipientId, val bundle: LogBundle) : RecipientFetch(bundle)
}
/**
* Simple class for [fetchRecipient] to pass back info that can be logged.
*/
private data class LogBundle(
val label: String,
val aci: ACI? = null,
val e164: String? = null,
val byAci: RecipientLogDetails? = null,
val byE164: RecipientLogDetails? = null
) {
fun label(label: String): LogBundle {
return this.copy(label = label)
}
}
/**
* Minimal info about a recipient that we'd want to log. Used in [fetchRecipient].
*/
private data class RecipientLogDetails(
val id: RecipientId,
val aci: ACI? = null,
val e164: String? = null
)
}