Incrementally insert MSL entries for legacy group sends.

This commit is contained in:
Greyson Parrelli
2021-07-02 17:25:31 -04:00
committed by Alex Hart
parent acc825971b
commit 7f0a0bef5a
14 changed files with 237 additions and 368 deletions

View File

@@ -10,6 +10,8 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -33,6 +35,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener;
import java.io.IOException;
import java.util.ArrayList;
@@ -45,6 +48,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public final class GroupSendUtil {
@@ -62,19 +67,19 @@ public final class GroupSendUtil {
*
* Messages sent this way, if failed to be decrypted by the receiving party, can be requested to be resent.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
*/
public static List<SendMessageResult> sendResendableDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@Nullable GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
ContentHint contentHint,
long relatedMessageId,
boolean isRelatedMessageMms,
@NonNull MessageId messageId,
@NonNull SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException
{
return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, relatedMessageId, isRelatedMessageMms), null);
return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, messageId), null);
}
/**
@@ -83,11 +88,12 @@ public final class GroupSendUtil {
*
* Messages sent this way, if failed to be decrypted by the receiving party, can *not* be requested to be resent.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
*/
@WorkerThread
public static List<SendMessageResult> sendUnresendableDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@Nullable GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
ContentHint contentHint,
@@ -100,10 +106,12 @@ public final class GroupSendUtil {
/**
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
*/
@WorkerThread
public static List<SendMessageResult> sendTypingMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@Nullable GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
@NonNull SignalServiceTypingMessage message,
@Nullable CancelationSignal cancelationSignal)
@@ -116,11 +124,12 @@ public final class GroupSendUtil {
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group.
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
*/
@WorkerThread
private static List<SendMessageResult> sendMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@Nullable GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
@NonNull SendOperation sendOperation,
@@ -147,7 +156,11 @@ public final class GroupSendUtil {
}
if (FeatureFlags.senderKey()) {
if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
if (groupId == null) {
Log.i(TAG, "Recipients not in a group. Using legacy.");
legacyTargets.addAll(senderKeyTargets);
senderKeyTargets.clear();
} else if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
Log.i(TAG, "All of our devices do not support sender key. Using legacy.");
legacyTargets.addAll(senderKeyTargets);
senderKeyTargets.clear();
@@ -168,11 +181,11 @@ public final class GroupSendUtil {
List<SendMessageResult> allResults = new ArrayList<>(allTargets.size());
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId);
if (senderKeyTargets.size() > 0) {
long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId);
long keyAge = System.currentTimeMillis() - keyCreateTime;
if (senderKeyTargets.size() > 0 && groupId != null) {
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId);
long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId);
long keyAge = System.currentTimeMillis() - keyCreateTime;
if (keyCreateTime != -1 && keyAge > MAX_KEY_AGE) {
Log.w(TAG, "Key is " + (keyAge) + " ms old (~" + TimeUnit.MILLISECONDS.toDays(keyAge) + " days). Rotating.");
@@ -190,7 +203,7 @@ public final class GroupSendUtil {
Log.d(TAG, "Successfully sent using sender key to " + successCount + "/" + targets.size() + " sender key targets.");
if (sendOperation.shouldIncludeInMessageLog()) {
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), senderKeyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms());
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), senderKeyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId());
}
} catch (NoSessionException e) {
Log.w(TAG, "No session. Falling back to legacy sends.", e);
@@ -212,16 +225,29 @@ public final class GroupSendUtil {
List<Optional<UnidentifiedAccessPair>> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList());
boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0;
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, cancelationSignal);
final MessageSendLogDatabase messageLogDatabase = DatabaseFactory.getMessageLogDatabase(context);
final AtomicLong entryId = new AtomicLong(-1);
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, result -> {
if (!includeInMessageLog) {
return;
}
synchronized (entryId) {
if (entryId.get() == -1) {
entryId.set(messageLogDatabase.insertIfPossible(recipients.requireRecipientId(result.getAddress()), sendOperation.getSentTimestamp(), result, sendOperation.getContentHint(), sendOperation.getRelatedMessageId()));
} else {
messageLogDatabase.addRecipientToExistingEntryIfPossible(entryId.get(), recipients.requireRecipientId(result.getAddress()), result);
}
}
}, cancelationSignal);
allResults.addAll(results);
int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count();
Log.d(TAG, "Successfully using 1:1 to " + successCount + "/" + targets.size() + " legacy targets.");
if (sendOperation.shouldIncludeInMessageLog()) {
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), legacyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms());
}
}
return allResults;
@@ -240,37 +266,39 @@ public final class GroupSendUtil {
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal)
throws IOException, UntrustedIdentityException;
@NonNull ContentHint getContentHint();
long getSentTimestamp();
boolean shouldIncludeInMessageLog();
long getRelatedMessageId();
boolean isRelatedMessageMms();
@NonNull MessageId getRelatedMessageId();
}
private static class DataSendOperation implements SendOperation {
private final SignalServiceDataMessage message;
private final ContentHint contentHint;
private final long relatedMessageId;
private final boolean isRelatedMessageMms;
private final MessageId relatedMessageId;
private final boolean resendable;
public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, long relatedMessageId, boolean isRelatedMessageMms) {
return new DataSendOperation(message, contentHint, true, relatedMessageId, isRelatedMessageMms);
public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, @NonNull MessageId relatedMessageId) {
return new DataSendOperation(message, contentHint, true, relatedMessageId);
}
public static DataSendOperation unresendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) {
return new DataSendOperation(message, contentHint, false, -1, false);
return new DataSendOperation(message, contentHint, false, null);
}
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, long relatedMessageId, boolean isRelatedMessageMms) {
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, @Nullable MessageId relatedMessageId) {
this.message = message;
this.contentHint = contentHint;
this.resendable = resendable;
this.relatedMessageId = relatedMessageId;
this.isRelatedMessageMms = isRelatedMessageMms;
if (resendable && relatedMessageId == null) {
throw new IllegalArgumentException("If a message is resendable, it must have a related message ID!");
}
}
@Override
@@ -289,10 +317,11 @@ public final class GroupSendUtil {
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal)
throws IOException, UntrustedIdentityException
{
return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message);
return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message, partialListener, cancelationSignal);
}
@Override
@@ -311,13 +340,12 @@ public final class GroupSendUtil {
}
@Override
public long getRelatedMessageId() {
return relatedMessageId;
}
@Override
public boolean isRelatedMessageMms() {
return isRelatedMessageMms;
public @NonNull MessageId getRelatedMessageId() {
if (relatedMessageId != null) {
return relatedMessageId;
} else {
throw new UnsupportedOperationException();
}
}
}
@@ -346,6 +374,7 @@ public final class GroupSendUtil {
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> access,
boolean isRecipientUpdate,
@Nullable PartialSendCompleteListener partialListener,
@Nullable CancelationSignal cancelationSignal)
throws IOException
{
@@ -369,13 +398,8 @@ public final class GroupSendUtil {
}
@Override
public long getRelatedMessageId() {
return -1;
}
@Override
public boolean isRelatedMessageMms() {
return false;
public @NonNull MessageId getRelatedMessageId() {
throw new UnsupportedOperationException();
}
}
@@ -386,10 +410,23 @@ public final class GroupSendUtil {
private final Map<RecipientId, Optional<UnidentifiedAccessPair>> accessById;
private final Map<RecipientId, SignalServiceAddress> addressById;
private final Map<UUID, RecipientId> idByUuid;
private final Map<String, RecipientId> idByE164;
RecipientData(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients);
this.addressById = mapAddresses(context, recipients);
this.idByUuid = new HashMap<>(recipients.size());
this.idByE164 = new HashMap<>(recipients.size());
for (Recipient recipient : recipients) {
if (recipient.hasUuid()) {
idByUuid.put(recipient.requireUuid(), recipient.getId());
}
if (recipient.hasE164()) {
idByE164.put(recipient.requireE164(), recipient.getId());
}
}
}
@NonNull SignalServiceAddress getAddress(@NonNull RecipientId id) {
@@ -404,6 +441,16 @@ public final class GroupSendUtil {
return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get();
}
@NonNull RecipientId requireRecipientId(@NonNull SignalServiceAddress address) {
if (address.getUuid().isPresent() && idByUuid.containsKey(address.getUuid().get())) {
return Objects.requireNonNull(idByUuid.get(address.getUuid().get()));
} else if (address.getNumber().isPresent() && idByE164.containsKey(address.getNumber().get())) {
return Objects.requireNonNull(idByE164.get(address.getNumber().get()));
} else {
throw new IllegalStateException();
}
}
private static @NonNull Map<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);

View File

@@ -287,8 +287,8 @@ public final class MessageContentProcessor {
ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false))
.then(GroupV2UpdateSelfProfileKeyJob.withQueueLimits(groupId.get().requireV2()))
.enqueue();
} else if (!sender.isPushV2Group()) {
Log.i(TAG, "Message was to a 1:1 or GV1 chat. Ensuring this user has our profile key.");
} else if (!sender.isGroup()) {
Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key.");
ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false))
.then(ProfileKeySendJob.create(context, DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender), true))
.enqueue();