Gracefully fallback to alternatives when group create fails.

This commit is contained in:
Cody Henthorne
2026-08-10 17:04:48 -04:00
parent 1fd343df93
commit bb35dc2370
2 changed files with 42 additions and 1 deletions
@@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.signal.storageservice.storage.protos.groups.AccessControl;
import org.signal.storageservice.storage.protos.groups.ExternalGroupCredential;
import org.signal.storageservice.storage.protos.groups.GroupChange;
@@ -887,6 +888,9 @@ final class GroupManagerV2 {
}
}
/**
* A 400 from the group service means a submitted profile key credential was expired.
*/
@WorkerThread
private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@@ -894,6 +898,28 @@ final class GroupManagerV2 {
@NonNull Collection<RecipientId> members,
int disappearingMessageTimerSeconds)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
try {
return createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessageTimerSeconds, false);
} catch (NonSuccessfulResponseCodeException e) {
if (e.code != 400) {
throw e;
}
Log.w(TAG, "[createGroupOnServer] Group was not accepted, refreshing all profile key credentials and retrying", e);
return createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessageTimerSeconds, true);
}
}
@WorkerThread
private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members,
int disappearingMessageTimerSeconds,
boolean forceRefreshProfileKeyCredentials)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) {
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 capability or we don't have their UUID");
@@ -901,6 +927,12 @@ final class GroupManagerV2 {
SignalDatabase.recipients().clearProfileKeyCredential(Recipient.self().getId());
if (forceRefreshProfileKeyCredentials) {
for (RecipientId member : members) {
SignalDatabase.recipients().clearProfileKeyCredential(member);
}
}
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.signal.core.models.ServiceId;
import java.io.IOException;
@@ -56,7 +57,15 @@ public class GroupCandidateHelper {
if (!candidate.hasValidProfileKeyCredential()) {
recipientTable.clearProfileKeyCredential(recipient.getId());
Optional<ExpiringProfileKeyCredential> credential = ProfileUtil.updateExpiringProfileKeyCredential(recipient);
Optional<ExpiringProfileKeyCredential> credential;
try {
credential = ProfileUtil.updateExpiringProfileKeyCredential(recipient);
} catch (NotFoundException e) {
Log.w(TAG, "Profile not found for " + recipient.getId() + ". Marking unregistered and falling back to an invite.");
recipientTable.markUnregistered(recipient.getId());
credential = Optional.empty();
}
if (credential.isPresent()) {
candidate = candidate.withExpiringProfileKeyCredential(credential.get());
} else {