Centralize recipient lookup in RecipientRepository.

This commit is contained in:
jeffrey-signal
2025-11-13 10:47:04 -05:00
committed by Cody Henthorne
parent 2e1291b3c3
commit 0e46ab33e8
14 changed files with 216 additions and 200 deletions

View File

@@ -6,12 +6,16 @@
package org.thoughtcrime.securesms.recipients
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
import org.thoughtcrime.securesms.util.SignalE164Util
import java.io.IOException
import kotlin.time.Duration.Companion.seconds
/**
* We operate on recipients many places, but sometimes we find ourselves performing the same recipient-related operations in several locations.
@@ -21,24 +25,68 @@ object RecipientRepository {
private val TAG = Log.tag(RecipientRepository::class.java)
/**
* Validates whether the provided [PhoneNumber] is a registered Signal user. Checks locally first, then queries the CDSI directory if needed.
*/
suspend fun lookup(phone: PhoneNumber): PhoneLookupResult {
return withContext(Dispatchers.IO) {
lookupNewE164(inputE164 = phone.value)
}
}
/**
* Validates whether the provided [RecipientId]s are registered Signal users.
*/
suspend fun lookup(recipientIds: List<RecipientId>): IdLookupResult {
val recipientsNeedingRegistrationCheck = Recipient
.resolvedList(recipientIds)
.filter { !it.isRegistered || !it.hasServiceId }
.toSet()
Log.d(TAG, "Need to do ${recipientsNeedingRegistrationCheck.size} registration checks.")
withContext(Dispatchers.IO) {
try {
ContactDiscovery.refresh(
context = AppDependencies.application,
recipients = recipientsNeedingRegistrationCheck.toList(),
notifyOfNewUsers = false,
timeoutMs = 30.seconds.inWholeMilliseconds
)
} catch (e: IOException) {
Log.w(TAG, "Failed to refresh registered status for ${recipientsNeedingRegistrationCheck.size} recipients", e)
}
}
val allRecipients = Recipient.resolvedList(recipientIds)
val (registeredRecipients, unregisteredRecipients) = allRecipients.partition { it.isRegistered && it.hasServiceId }
return if (unregisteredRecipients.isNotEmpty()) {
Log.w(TAG, "Found ${unregisteredRecipients.size} non-Signal users: ${unregisteredRecipients.joinToString(", ") { it.id.toString() }}")
IdLookupResult.FoundSome(found = registeredRecipients, notFound = unregisteredRecipients)
} else {
IdLookupResult.FoundAll(registeredRecipients)
}
}
/**
* Attempts to lookup a potentially-new recipient by their e164.
* We will check locally first for a potential match, but may end up hitting the network.
* This will not create a new recipient if we could not find it in the CDSI directory.
*/
@Deprecated("Use [RecipientRepository.lookup(PhoneNumber)] instead.")
@WorkerThread
@JvmStatic
fun lookupNewE164(inputE164: String): LookupResult {
fun lookupNewE164(inputE164: String): PhoneLookupResult {
val e164 = SignalE164Util.formatAsE164(inputE164)
if (e164 == null || !NumberUtil.isVisuallyValidNumber(e164)) {
return LookupResult.InvalidEntry
return PhoneLookupResult.InvalidPhone(inputE164)
}
val matchingFullRecipientId = SignalDatabase.recipients.getByE164IfRegisteredAndDiscoverable(e164)
if (matchingFullRecipientId != null) {
Log.i(TAG, "Already have a full, discoverable recipient for $e164. $matchingFullRecipientId")
return LookupResult.Success(matchingFullRecipientId)
return PhoneLookupResult.Found(recipient = Recipient.resolved(matchingFullRecipientId), phone = PhoneNumber(e164))
}
Log.i(TAG, "Need to lookup up $e164 with CDSI.")
@@ -46,19 +94,32 @@ object RecipientRepository {
return try {
val result = ContactDiscovery.lookupE164(e164)
if (result == null) {
LookupResult.NotFound()
PhoneLookupResult.NotFound(PhoneNumber(e164))
} else {
LookupResult.Success(result.recipientId)
PhoneLookupResult.Found(recipient = Recipient.resolved(result.recipientId), phone = PhoneNumber(e164))
}
} catch (e: IOException) {
} catch (_: IOException) {
return LookupResult.NetworkError
}
}
sealed interface LookupResult {
data class Success(val recipientId: RecipientId) : LookupResult
object InvalidEntry : LookupResult
data class NotFound(val recipientId: RecipientId = RecipientId.UNKNOWN) : LookupResult
object NetworkError : LookupResult
sealed interface Success : LookupResult
sealed interface Failure : LookupResult
data object NetworkError : PhoneLookupResult, IdLookupResult, Failure
}
sealed interface PhoneLookupResult : LookupResult {
sealed interface NoResult : PhoneLookupResult, LookupResult.Failure
data class Found(val recipient: Recipient, val phone: PhoneNumber) : PhoneLookupResult, LookupResult.Success
data class NotFound(val phone: PhoneNumber) : NoResult
data class InvalidPhone(val invalidValue: String) : NoResult
}
sealed interface IdLookupResult : LookupResult {
data class FoundAll(val found: List<Recipient>) : IdLookupResult, LookupResult.Success
data class FoundSome(val found: List<Recipient>, val notFound: List<Recipient>) : IdLookupResult, LookupResult.Failure
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import org.signal.core.ui.compose.Dialogs
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.recipients.RecipientRepository.IdLookupResult
import org.thoughtcrime.securesms.recipients.RecipientRepository.PhoneLookupResult
/**
* Handles displaying a message to the user when a recipient lookup fails.
*/
@Composable
fun RecipientLookupFailureMessage(
failure: RecipientRepository.LookupResult.Failure,
onDismissed: () -> Unit
) {
val context: Context = LocalContext.current
when (failure) {
is PhoneLookupResult.NotFound,
is PhoneLookupResult.InvalidPhone -> {
val phoneDisplayText: String = when (failure) {
is PhoneLookupResult.NotFound -> failure.phone.displayText
is PhoneLookupResult.InvalidPhone -> failure.invalidValue
}
Dialogs.SimpleMessageDialog(
message = stringResource(R.string.RecipientLookup_error__s_is_not_a_signal_user, phoneDisplayText),
dismiss = stringResource(android.R.string.ok),
onDismiss = { onDismissed() }
)
}
is IdLookupResult.FoundSome -> Dialogs.SimpleMessageDialog(
message = pluralStringResource(
id = R.plurals.RecipientLookup_error__not_signal_users,
count = failure.notFound.size,
failure.notFound.joinToString(", ") { it.getDisplayName(context) }
),
dismiss = stringResource(android.R.string.ok),
onDismiss = { onDismissed() }
)
is RecipientRepository.LookupResult.NetworkError -> Dialogs.SimpleMessageDialog(
message = stringResource(R.string.NetworkFailure__network_error_check_your_connection_and_try_again),
dismiss = stringResource(android.R.string.ok),
onDismiss = { onDismissed() }
)
}
}

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
@@ -71,19 +72,18 @@ class FindByViewModel(
}
}
@WorkerThread
private fun performPhoneLookup(): FindByResult {
private suspend fun performPhoneLookup(): FindByResult {
val stateSnapshot = state.value
val countryCode = stateSnapshot.selectedCountry.countryCode
val nationalNumber = stateSnapshot.userEntry.removePrefix(countryCode.toString())
val e164 = "+$countryCode$nationalNumber"
return when (val result = RecipientRepository.lookupNewE164(e164)) {
RecipientRepository.LookupResult.InvalidEntry -> FindByResult.InvalidEntry
RecipientRepository.LookupResult.NetworkError -> FindByResult.NetworkError
is RecipientRepository.LookupResult.NotFound -> FindByResult.NotFound(result.recipientId)
is RecipientRepository.LookupResult.Success -> FindByResult.Success(result.recipientId)
return when (val result = RecipientRepository.lookup(PhoneNumber(e164))) {
is RecipientRepository.PhoneLookupResult.InvalidPhone -> FindByResult.InvalidEntry
is RecipientRepository.PhoneLookupResult.NotFound -> FindByResult.NotFound()
is RecipientRepository.PhoneLookupResult.Found -> FindByResult.Success(result.recipient.id)
is RecipientRepository.LookupResult.NetworkError -> FindByResult.NetworkError
}
}