Do not attempt to send to unregistered users when using CDS flag.

CDS is slow, and unregistered users will always trigger a CDS lookup on
send (since we can't get their UUID).

This starts skipping sends to unregistered users and shortens the time
window to do a full CDS lookup from every 12 hours to every 6 hours.
master
Greyson Parrelli 2020-08-31 11:33:35 -04:00
parent 1e37951701
commit 4714895c59
11 changed files with 149 additions and 75 deletions

View File

@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.RecipientDetails;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -92,61 +93,35 @@ public class DirectoryHelper {
return;
}
Stopwatch stopwatch = new Stopwatch("full");
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
stopwatch.split("network");
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
notifyNewUsers(context, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
refreshNumbers(context, databaseNumbers, systemNumbers, notifyOfNewUsers);
StorageSyncHelper.scheduleSyncForDataChange();
}
stopwatch.split("disk");
stopwatch.stop(TAG);
@WorkerThread
public static void refreshDirectoryFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean notifyOfNewUsers) throws IOException {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
for (Recipient recipient : recipients) {
if (recipient.hasUuid() && !recipient.hasE164()) {
if (isUuidRegistered(context, recipient)) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
}
}
Set<String> numbers = Stream.of(recipients)
.filter(Recipient::hasE164)
.map(Recipient::requireE164)
.collect(Collectors.toSet());
refreshNumbers(context, numbers, numbers, notifyOfNewUsers);
}
@WorkerThread
@ -231,6 +206,61 @@ public class DirectoryHelper {
return newRegisteredState;
}
@WorkerThread
private static void refreshNumbers(@NonNull Context context, @NonNull Set<String> databaseNumbers, @NonNull Set<String> systemNumbers, boolean notifyOfNewUsers) throws IOException {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
if (allNumbers.isEmpty()) {
Log.w(TAG, "No numbers to refresh!");
return;
}
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
notifyNewUsers(context, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);

View File

@ -56,7 +56,7 @@ public class ProfileKeySendJob extends BaseJob {
throw new AssertionError("Do not send profile keys directly for GV2");
}
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants())).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();
recipients.remove(Recipient.self().getId());

View File

@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@ -195,7 +197,13 @@ public final class PushGroupSendJob extends PushSendJob {
List<Pair<RecipientId, Boolean>> successUnidentifiedStatus = Stream.of(successes).map(result -> new Pair<>(findId(result.getAddress(), idByE164, idByUuid), result.getSuccess().isUnidentified())).toList();
Set<RecipientId> successIds = Stream.of(successUnidentifiedStatus).map(Pair::first).collect(Collectors.toSet());
List<NetworkFailure> resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList();
List<IdentityKeyMismatch> resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList();
List<IdentityKeyMismatch> resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList();
List<Recipient> unregisteredRecipients = Stream.of(results).filter(SendMessageResult::isUnregisteredFailure).map(result -> Recipient.externalPush(context, result.getAddress())).toList();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
for (Recipient unregistered : unregisteredRecipients) {
recipientDatabase.markUnregistered(unregistered.getId());
}
for (NetworkFailure resolvedFailure : resolvedNetworkFailures) {
database.removeFailure(messageId, resolvedFailure);
@ -366,7 +374,10 @@ public final class PushGroupSendJob extends PushSendJob {
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
if (!destinations.isEmpty()) {
return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).map(Recipient::resolved).toList();
return RecipientUtil.getEligibleForSending(Stream.of(destinations)
.map(GroupReceiptInfo::getRecipientId)
.map(Recipient::resolved)
.toList());
}
List<Recipient> members = Stream.of(DatabaseFactory.getGroupDatabase(context)
@ -377,8 +388,8 @@ public final class PushGroupSendJob extends PushSendJob {
if (members.size() > 0) {
Log.w(TAG, "No destinations found for group message " + groupId + " using current group membership");
}
return members;
return RecipientUtil.getEligibleForSending(members);
}
public static class Factory implements Job.Factory<PushGroupSendJob> {

View File

@ -11,6 +11,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
@ -75,7 +77,15 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
Set<RecipientId> recipients = Stream.concat(Stream.of(memberUuids), Stream.of(pendingUuids))
.filter(uuid -> !UuidUtil.UNKNOWN_UUID.equals(uuid))
.filter(uuid -> !Recipient.self().getUuid().get().equals(uuid))
.map(uuid -> RecipientId.from(uuid, null))
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
.filter(recipient -> {
if (FeatureFlags.cds()) {
return recipient.getRegistered() != RecipientDatabase.RegisteredState.NOT_REGISTERED;
} else {
return true;
}
})
.map(Recipient::getId)
.collect(Collectors.toSet());
MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();

View File

@ -75,7 +75,7 @@ public class ReactionSendJob extends BaseJob {
throw new AssertionError("We have a message, but couldn't find the thread!");
}
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants())).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();
recipients.remove(Recipient.self().getId());

View File

@ -65,7 +65,7 @@ public class RemoteDeleteSendJob extends BaseJob {
throw new AssertionError("We have a message, but couldn't find the thread!");
}
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants())).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();
recipients.remove(Recipient.self().getId());

View File

@ -9,6 +9,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.CancelationException;
@ -104,7 +106,9 @@ public class TypingSendJob extends BaseJob {
groupId = Optional.of(recipient.requireGroupId().getDecodedId());
}
recipients = Stream.of(recipients).map(Recipient::resolve).toList();
recipients = RecipientUtil.getEligibleForSending(Stream.of(recipients)
.map(Recipient::resolve)
.toList());
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);

View File

@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import com.google.android.gms.common.Feature;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -29,6 +30,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class RecipientUtil {
@ -38,7 +40,8 @@ public class RecipientUtil {
/**
* This method will do it's best to craft a fully-populated {@link SignalServiceAddress} based on
* the provided recipient. This includes performing a possible network request if no UUID is
* available.
* available. If the request to get a UUID fails, the exception is swallowed an an E164-only
* recipient is returned.
*/
@WorkerThread
public static @NonNull SignalServiceAddress toSignalServiceAddressBestEffort(@NonNull Context context, @NonNull Recipient recipient) {
@ -53,7 +56,7 @@ public class RecipientUtil {
/**
* This method will do it's best to craft a fully-populated {@link SignalServiceAddress} based on
* the provided recipient. This includes performing a possible network request if no UUID is
* available.
* available. If the request to get a UUID fails, an IOException is thrown.
*/
@WorkerThread
public static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient)
@ -79,25 +82,27 @@ public class RecipientUtil {
public static @NonNull List<SignalServiceAddress> toSignalServiceAddresses(@NonNull Context context, @NonNull List<RecipientId> recipients)
throws IOException
{
List<SignalServiceAddress> addresses = new ArrayList<>(recipients.size());
for (RecipientId id : recipients) {
addresses.add(toSignalServiceAddress(context, Recipient.resolved(id)));
}
return addresses;
return toSignalServiceAddressesFromResolved(context, Recipient.resolvedList(recipients));
}
public static @NonNull List<SignalServiceAddress> toSignalServiceAddressesFromResolved(@NonNull Context context, @NonNull List<Recipient> recipients)
throws IOException
{
List<SignalServiceAddress> addresses = new ArrayList<>(recipients.size());
if (FeatureFlags.cds()) {
List<Recipient> recipientsWithoutUuids = Stream.of(recipients)
.map(Recipient::resolve)
.filterNot(Recipient::hasUuid)
.toList();
for (Recipient recipient : recipients) {
addresses.add(toSignalServiceAddress(context, recipient));
if (recipientsWithoutUuids.size() > 0) {
DirectoryHelper.refreshDirectoryFor(context, recipientsWithoutUuids, false);
}
}
return addresses;
return Stream.of(recipients)
.map(Recipient::resolve)
.map(r -> new SignalServiceAddress(r.getUuid().orNull(), r.getE164().orNull()))
.toList();
}
public static boolean isBlockable(@NonNull Recipient recipient) {
@ -105,6 +110,16 @@ public class RecipientUtil {
return resolved.isPushGroup() || resolved.hasServiceIdentifier();
}
public static List<Recipient> getEligibleForSending(@NonNull List<Recipient> recipients) {
if (FeatureFlags.cds()) {
return Stream.of(recipients)
.filter(r -> r.getRegistered() != RegisteredState.NOT_REGISTERED)
.toList();
} else {
return recipients;
}
}
/**
* You can call this for non-groups and not have to handle any network errors.
*/

View File

@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit;
public class DirectoryRefreshListener extends PersistentAlarmManagerListener {
private static final long INTERVAL = TimeUnit.HOURS.toMillis(12);
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
@Override
protected long getNextScheduledExecutionTime(Context context) {

View File

@ -59,7 +59,7 @@ public final class FeatureFlags {
private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion";
private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion";
private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize";
private static final String CDS = "android.cds.4";
private static final String CDS_VERSION = "android.cdsVersion";
private static final String INTERNAL_USER = "android.internalUser";
private static final String MENTIONS = "android.mentions";
private static final String VERIFY_V2 = "android.verifyV2";
@ -77,7 +77,7 @@ public final class FeatureFlags {
GROUPS_V2_CAPACITY,
GROUPS_V2_JOIN_VERSION,
GROUPS_V2_LINKS_VERSION,
CDS,
CDS_VERSION,
INTERNAL_USER,
USERNAMES,
MENTIONS,
@ -106,7 +106,7 @@ public final class FeatureFlags {
GROUPS_V2_CREATE,
GROUPS_V2_JOIN_VERSION,
VERIFY_V2,
CDS
CDS_VERSION
);
/**
@ -266,7 +266,7 @@ public final class FeatureFlags {
/** Whether or not to use the new contact discovery service endpoint, which supports UUIDs. */
public static boolean cds() {
return getBoolean(CDS, false);
return getVersionFlag(CDS_VERSION) == VersionFlag.ON;
}
/** Whether or not we allow mentions send support in groups. */

View File

@ -368,6 +368,10 @@ public class SignalServiceAccountManager {
public Map<String, UUID> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
if (e164numbers.isEmpty()) {
return Collections.emptyMap();
}
try {
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Map<String, RemoteAttestation> attestations = RemoteAttestationUtil.getAndVerifyMultiRemoteAttestation(pushServiceSocket,