mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Add ability to reject group invite by PNI.
This commit is contained in:
@@ -6,8 +6,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
@@ -24,7 +22,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
@@ -40,7 +38,8 @@ public final class GroupManager {
|
||||
private static final String TAG = Log.tag(GroupManager.class);
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
||||
public static @NonNull GroupActionResult createGroup(@NonNull ServiceId authServiceId,
|
||||
@NonNull Context context,
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable byte[] avatar,
|
||||
@Nullable String name,
|
||||
@@ -54,7 +53,7 @@ public final class GroupManager {
|
||||
if (shouldAttemptToCreateV2) {
|
||||
try {
|
||||
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
|
||||
return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer);
|
||||
return groupCreator.createGroup(authServiceId, memberIds, name, avatar, disappearingMessagesTimer);
|
||||
}
|
||||
} catch (MembershipNotSuitableForV2Exception e) {
|
||||
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);
|
||||
@@ -178,6 +177,7 @@ public final class GroupManager {
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void updateGroupFromServer(@NonNull Context context,
|
||||
@NonNull ServiceId authServiceId,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
int revision,
|
||||
long timestamp,
|
||||
@@ -185,17 +185,18 @@ public final class GroupManager {
|
||||
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
|
||||
updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange);
|
||||
updater.updateLocalToServerRevision(authServiceId, revision, timestamp, signedGroupChange);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
|
||||
@NonNull ServiceId authServiceserviceId,
|
||||
@NonNull GroupMasterKey groupMasterKey)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
new GroupManagerV2(context).groupServerQuery(groupMasterKey);
|
||||
new GroupManagerV2(context).groupServerQuery(authServiceserviceId, groupMasterKey);
|
||||
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
|
||||
} catch (GroupNotAMemberException e) {
|
||||
return V2GroupServerStatus.NOT_A_MEMBER;
|
||||
@@ -210,11 +211,12 @@ public final class GroupManager {
|
||||
* If it fails to get the exact version, it will give the latest.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static DecryptedGroup addedGroupVersion(@NonNull Context context,
|
||||
public static DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId,
|
||||
@NonNull Context context,
|
||||
@NonNull GroupMasterKey groupMasterKey)
|
||||
throws IOException, GroupDoesNotExistException, GroupNotAMemberException
|
||||
{
|
||||
return new GroupManagerV2(context).addedGroupVersion(groupMasterKey);
|
||||
return new GroupManagerV2(context).addedGroupVersion(authServiceId, groupMasterKey);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -268,12 +270,13 @@ public final class GroupManager {
|
||||
|
||||
@WorkerThread
|
||||
public static void revokeInvites(@NonNull Context context,
|
||||
@NonNull ServiceId authServiceId,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
editor.revokeInvites(uuidCipherTexts);
|
||||
editor.revokeInvites(authServiceId, uuidCipherTexts, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||
@@ -97,6 +98,7 @@ final class GroupManagerV2 {
|
||||
private final GroupsV2Authorization authorization;
|
||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private final ACI selfAci;
|
||||
private final PNI selfPni;
|
||||
private final GroupCandidateHelper groupCandidateHelper;
|
||||
private final SendGroupUpdateHelper sendGroupUpdateHelper;
|
||||
|
||||
@@ -108,6 +110,7 @@ final class GroupManagerV2 {
|
||||
ApplicationDependencies.getGroupsV2Authorization(),
|
||||
ApplicationDependencies.getGroupsV2StateProcessor(),
|
||||
SignalStore.account().requireAci(),
|
||||
SignalStore.account().requirePni(),
|
||||
new GroupCandidateHelper(context),
|
||||
new SendGroupUpdateHelper(context));
|
||||
}
|
||||
@@ -119,6 +122,7 @@ final class GroupManagerV2 {
|
||||
GroupsV2Authorization authorization,
|
||||
GroupsV2StateProcessor groupsV2StateProcessor,
|
||||
ACI selfAci,
|
||||
PNI selfPni,
|
||||
GroupCandidateHelper groupCandidateHelper,
|
||||
SendGroupUpdateHelper sendGroupUpdateHelper)
|
||||
{
|
||||
@@ -129,6 +133,7 @@ final class GroupManagerV2 {
|
||||
this.authorization = authorization;
|
||||
this.groupsV2StateProcessor = groupsV2StateProcessor;
|
||||
this.selfAci = selfAci;
|
||||
this.selfPni = selfPni;
|
||||
this.groupCandidateHelper = groupCandidateHelper;
|
||||
this.sendGroupUpdateHelper = sendGroupUpdateHelper;
|
||||
}
|
||||
@@ -204,18 +209,18 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void groupServerQuery(@NonNull GroupMasterKey groupMasterKey)
|
||||
void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
|
||||
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
|
||||
.getCurrentGroupStateFromServer();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull DecryptedGroup addedGroupVersion(@NonNull GroupMasterKey groupMasterKey)
|
||||
@NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
|
||||
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||
{
|
||||
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(groupMasterKey);
|
||||
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey);
|
||||
DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
|
||||
|
||||
if (latest.getRevision() == 0) {
|
||||
@@ -233,7 +238,7 @@ final class GroupManagerV2 {
|
||||
if (joinedVersion != null) {
|
||||
return joinedVersion;
|
||||
} else {
|
||||
Log.w(TAG, "Unable to retreive exact version joined at, using latest");
|
||||
Log.w(TAG, "Unable to retrieve exact version joined at, using latest");
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
@@ -269,7 +274,8 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
|
||||
@NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId,
|
||||
@NonNull Collection<RecipientId> members,
|
||||
@Nullable String name,
|
||||
@Nullable byte[] avatar,
|
||||
int disappearingMessagesTimer)
|
||||
@@ -285,7 +291,7 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
|
||||
GroupId.V2 groupId = groupDatabase.create(authServiceId, masterKey, decryptedGroup);
|
||||
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
@@ -306,6 +312,7 @@ final class GroupManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
final class GroupEditor extends LockOwner {
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
@@ -340,35 +347,35 @@ final class GroupManagerV2 {
|
||||
groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates);
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupTimerChange(expirationTime));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateAttributesRights(@NonNull GroupAccessControl newRights)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateMembershipRights(@NonNull GroupAccessControl newRights)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateAnnouncementGroup(boolean isAnnouncementGroup)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -390,7 +397,7 @@ final class GroupManagerV2 {
|
||||
.setAvatar(cdnKey));
|
||||
}
|
||||
|
||||
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change);
|
||||
GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(selfAci, change);
|
||||
|
||||
if (avatarChanged) {
|
||||
AvatarHelper.setAvatar(context, Recipient.externalGroupExact(context, groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
|
||||
@@ -404,10 +411,10 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts)
|
||||
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection<UuidCiphertext> uuidCipherTexts, boolean sendToMembers)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)));
|
||||
return commitChangeWithConflictResolution(authServiceId, groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)), false, sendToMembers);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -418,7 +425,7 @@ final class GroupManagerV2 {
|
||||
.map(r -> Recipient.resolved(r).requireServiceId().uuid())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createApproveGroupJoinRequest(uuids));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createApproveGroupJoinRequest(uuids));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -429,7 +436,7 @@ final class GroupManagerV2 {
|
||||
.map(r -> Recipient.resolved(r).requireServiceId().uuid())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -438,33 +445,47 @@ final class GroupManagerV2 {
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
return commitChangeWithConflictResolution(groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult leaveGroup()
|
||||
void leaveGroup()
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.getGroup(groupId).get();
|
||||
List<DecryptedPendingMember> pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList();
|
||||
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
|
||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
Optional<DecryptedMember> selfMember = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> aciPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> pniPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfPni.uuid());
|
||||
Optional<DecryptedPendingMember> selfPendingMember = Optional.empty();
|
||||
ServiceId serviceId = selfAci;
|
||||
|
||||
if (aciPendingMember.isPresent()) {
|
||||
selfPendingMember = aciPendingMember;
|
||||
} else if (pniPendingMember.isPresent() && !selfMember.isPresent()) {
|
||||
selfPendingMember = pniPendingMember;
|
||||
serviceId = selfPni;
|
||||
}
|
||||
|
||||
if (selfPendingMember.isPresent()) {
|
||||
try {
|
||||
return revokeInvites(Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray())));
|
||||
revokeInvites(serviceId, Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray())), false);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} else if (selfMember.isPresent()) {
|
||||
ejectMember(serviceId, true, false);
|
||||
} else {
|
||||
return ejectMember(ServiceId.from(selfAci.uuid()), true, false);
|
||||
Log.i(TAG, "Unable to leave group we are not pending or in");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked, boolean ban)
|
||||
@NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId authServiceId, boolean allowWhenBlocked, boolean ban)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid()),
|
||||
return commitChangeWithConflictResolution(authServiceId,
|
||||
groupOperations.createRemoveMembersChange(Collections.singleton(authServiceId.uuid()),
|
||||
ban,
|
||||
ban ? v2GroupProperties.getDecryptedGroup().getBannedMembersList()
|
||||
: Collections.emptyList()),
|
||||
@@ -475,10 +496,9 @@ final class GroupManagerV2 {
|
||||
@NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection<RecipientId> newAdmins)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
Recipient self = Recipient.self();
|
||||
List<UUID> newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).requireServiceId().uuid()).toList();
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(),
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(),
|
||||
newAdminRecipients));
|
||||
}
|
||||
|
||||
@@ -510,7 +530,7 @@ final class GroupManagerV2 {
|
||||
return null;
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireProfileKeyCredential()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -532,7 +552,7 @@ final class GroupManagerV2 {
|
||||
return null;
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireProfileKeyCredential()));
|
||||
}
|
||||
|
||||
public GroupManager.GroupActionResult ban(UUID uuid)
|
||||
@@ -541,20 +561,20 @@ final class GroupManagerV2 {
|
||||
ByteString uuidByteString = UuidUtil.toByteString(uuid);
|
||||
boolean rejectJoinRequest = v2GroupProperties.getDecryptedGroup().getRequestingMembersList().stream().anyMatch(m -> m.getUuid().equals(uuidByteString));
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList()));
|
||||
}
|
||||
|
||||
public GroupManager.GroupActionResult unban(Set<UUID> uuids)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createUnbanUuidsChange(uuids));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createUnbanUuidsChange(uuids));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public GroupManager.GroupActionResult cycleGroupLinkPassword()
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -581,7 +601,7 @@ final class GroupManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
commitChangeWithConflictResolution(change);
|
||||
commitChangeWithConflictResolution(selfAci, change);
|
||||
|
||||
if (state != GroupManager.GroupLinkState.DISABLED) {
|
||||
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.requireGroup(groupId).requireV2GroupProperties();
|
||||
@@ -594,26 +614,32 @@ final class GroupManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
return commitChangeWithConflictResolution(change, false);
|
||||
return commitChangeWithConflictResolution(authServiceId, change, false);
|
||||
}
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
change.setSourceUuid(UuidUtil.toByteString(selfAci.uuid()));
|
||||
return commitChangeWithConflictResolution(authServiceId, change, allowWhenBlocked, true);
|
||||
}
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
change.setSourceUuid(UuidUtil.toByteString(authServiceId.uuid()));
|
||||
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
return commitChange(change, allowWhenBlocked);
|
||||
return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers);
|
||||
} catch (GroupPatchNotAcceptedException e) {
|
||||
throw new GroupChangeFailedException(e);
|
||||
} catch (ConflictException e) {
|
||||
Log.w(TAG, "Invalid group patch or conflict", e);
|
||||
|
||||
change = resolveConflict(change);
|
||||
change = resolveConflict(authServiceId, change);
|
||||
|
||||
if (GroupChangeUtil.changeIsEmpty(change.build())) {
|
||||
Log.i(TAG, "Change is empty after conflict resolution");
|
||||
@@ -628,10 +654,10 @@ final class GroupManagerV2 {
|
||||
throw new GroupChangeFailedException("Unable to apply change to group after conflicts");
|
||||
}
|
||||
|
||||
private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions.Builder change)
|
||||
private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
|
||||
throws IOException, GroupNotAMemberException, GroupChangeFailedException
|
||||
{
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(authServiceId, groupMasterKey)
|
||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
|
||||
|
||||
if (groupUpdateResult.getLatestServer() == null) {
|
||||
@@ -652,14 +678,14 @@ final class GroupManagerV2 {
|
||||
GroupChange.Actions changeActions = change.build();
|
||||
|
||||
return GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(),
|
||||
groupOperations.decryptChange(changeActions, selfAci.uuid()),
|
||||
groupOperations.decryptChange(changeActions, authServiceId.uuid()),
|
||||
changeActions);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException ex) {
|
||||
throw new GroupChangeFailedException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
|
||||
private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
@@ -676,8 +702,9 @@ final class GroupManagerV2 {
|
||||
|
||||
previousGroupState = v2GroupProperties.getDecryptedGroup();
|
||||
|
||||
GroupChange signedGroupChange = commitToServer(changeActions);
|
||||
GroupChange signedGroupChange = commitToServer(authServiceId, changeActions);
|
||||
try {
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
|
||||
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
|
||||
@@ -688,18 +715,18 @@ final class GroupManagerV2 {
|
||||
groupDatabase.update(groupId, decryptedGroupState);
|
||||
|
||||
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange);
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers);
|
||||
int newMembersCount = decryptedChange.getNewMembersCount();
|
||||
List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList());
|
||||
|
||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
|
||||
}
|
||||
|
||||
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
|
||||
private @NonNull GroupChange commitToServer(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfAci, groupSecretParams), Optional.empty());
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(authServiceId, groupSecretParams), Optional.empty());
|
||||
} catch (NotInGroupException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupNotAMemberException(e);
|
||||
@@ -724,10 +751,10 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange)
|
||||
void updateLocalToServerRevision(@NonNull ServiceId authServiceId, int revision, long timestamp, @Nullable byte[] signedGroupChange)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
|
||||
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
|
||||
}
|
||||
|
||||
@@ -880,7 +907,7 @@ final class GroupManagerV2 {
|
||||
|
||||
groupDatabase.update(groupId, updatedGroup);
|
||||
} else {
|
||||
groupDatabase.create(groupMasterKey, decryptedGroup);
|
||||
groupDatabase.create(selfAci, groupMasterKey, decryptedGroup);
|
||||
Log.i(TAG, "Created local group with placeholder");
|
||||
}
|
||||
|
||||
@@ -924,7 +951,7 @@ final class GroupManagerV2 {
|
||||
throws GroupChangeFailedException, IOException
|
||||
{
|
||||
try {
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(selfAci, groupMasterKey)
|
||||
.updateLocalGroupToRevision(decryptedChange.getRevision(),
|
||||
System.currentTimeMillis(),
|
||||
decryptedChange);
|
||||
@@ -956,6 +983,7 @@ final class GroupManagerV2 {
|
||||
throws GroupChangeFailedException
|
||||
{
|
||||
try {
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
return groupOperations.decryptChange(signedGroupChange, false).get();
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -1006,7 +1034,7 @@ final class GroupManagerV2 {
|
||||
throw new MembershipNotSuitableForV2Exception("No profile key credential for self");
|
||||
}
|
||||
|
||||
ProfileKeyCredential profileKeyCredential = self.getProfileKeyCredential().get();
|
||||
ProfileKeyCredential profileKeyCredential = self.requireProfileKeyCredential();
|
||||
|
||||
GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
||||
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
||||
@@ -1123,6 +1151,7 @@ final class GroupManagerV2 {
|
||||
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
|
||||
|
||||
try {
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange);
|
||||
|
||||
@@ -1226,6 +1255,7 @@ final class GroupManagerV2 {
|
||||
|
||||
return new RecipientAndThread(groupRecipient, -1);
|
||||
} else {
|
||||
//noinspection IfStatementWithIdenticalBranches
|
||||
if (sendToMembers) {
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null, null);
|
||||
return new RecipientAndThread(groupRecipient, threadId);
|
||||
|
||||
@@ -2,10 +2,23 @@ package org.thoughtcrime.securesms.groups;
|
||||
|
||||
public final class GroupNotAMemberException extends GroupChangeException {
|
||||
|
||||
private final boolean likelyPendingMember;
|
||||
|
||||
public GroupNotAMemberException(Throwable throwable) {
|
||||
super(throwable);
|
||||
this.likelyPendingMember = false;
|
||||
}
|
||||
|
||||
public GroupNotAMemberException(GroupNotAMemberException throwable, boolean likelyPendingMember) {
|
||||
super(throwable.getCause() != null ? throwable.getCause() : throwable);
|
||||
this.likelyPendingMember = likelyPendingMember;
|
||||
}
|
||||
|
||||
GroupNotAMemberException() {
|
||||
this.likelyPendingMember = false;
|
||||
}
|
||||
|
||||
public boolean isLikelyPendingMember() {
|
||||
return likelyPendingMember;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class GroupsV1MigrationUtil {
|
||||
throw new InvalidMigrationStateException();
|
||||
}
|
||||
|
||||
switch (GroupManager.v2GroupStatus(context, gv2MasterKey)) {
|
||||
switch (GroupManager.v2GroupStatus(context, SignalStore.account().getAci(), gv2MasterKey)) {
|
||||
case DOES_NOT_EXIST:
|
||||
Log.i(TAG, "Group does not exist on the service.");
|
||||
|
||||
@@ -172,7 +172,7 @@ public final class GroupsV1MigrationUtil {
|
||||
try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock()){
|
||||
DecryptedGroup decryptedGroup;
|
||||
try {
|
||||
decryptedGroup = GroupManager.addedGroupVersion(context, gv1Id.deriveV2MigrationMasterKey());
|
||||
decryptedGroup = GroupManager.addedGroupVersion(SignalStore.account().requireAci(), context, gv1Id.deriveV2MigrationMasterKey());
|
||||
} catch (GroupDoesNotExistException e) {
|
||||
throw new IOException("[Local] The group should exist already!");
|
||||
} catch (GroupNotAMemberException e) {
|
||||
@@ -186,7 +186,7 @@ public final class GroupsV1MigrationUtil {
|
||||
|
||||
Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision());
|
||||
try {
|
||||
GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
|
||||
GroupManager.updateGroupFromServer(context, SignalStore.account().requireAci(), gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
|
||||
} catch (GroupChangeBusyException | GroupNotAMemberException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
@@ -6,37 +6,53 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class GroupsV2Authorization {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2Authorization.class);
|
||||
|
||||
private final ValueCache cache;
|
||||
private final ValueCache aciCache;
|
||||
private final ValueCache pniCache;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
|
||||
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache cache) {
|
||||
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache aciCache, @NonNull ValueCache pniCache) {
|
||||
this.groupsV2Api = groupsV2Api;
|
||||
this.cache = cache;
|
||||
this.aciCache = aciCache;
|
||||
this.pniCache = pniCache;
|
||||
}
|
||||
|
||||
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ACI self,
|
||||
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
|
||||
@NonNull GroupSecretParams groupSecretParams)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
boolean isPni = Objects.equals(authServiceId, SignalStore.account().getPni());
|
||||
ValueCache cache = isPni ? pniCache : aciCache;
|
||||
|
||||
return getAuthorizationForToday(authServiceId, cache, groupSecretParams, !isPni);
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
|
||||
@NonNull ValueCache cache,
|
||||
@NonNull GroupSecretParams groupSecretParams,
|
||||
boolean isAci)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
final int today = currentTimeDays();
|
||||
|
||||
Map<Integer, AuthCredentialResponse> credentials = cache.read();
|
||||
|
||||
try {
|
||||
return getAuthorization(self, groupSecretParams, credentials, today);
|
||||
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.i(TAG, "Auth out of date, will update auth and try again");
|
||||
cache.clear();
|
||||
@@ -46,11 +62,11 @@ public class GroupsV2Authorization {
|
||||
}
|
||||
|
||||
Log.i(TAG, "Getting new auth credential responses");
|
||||
credentials = groupsV2Api.getCredentials(today);
|
||||
credentials = groupsV2Api.getCredentials(today, isAci);
|
||||
cache.write(credentials);
|
||||
|
||||
try {
|
||||
return getAuthorization(self, groupSecretParams, credentials, today);
|
||||
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.w(TAG, "The credentials returned did not include the day requested");
|
||||
throw new IOException("Failed to get credentials");
|
||||
@@ -58,14 +74,15 @@ public class GroupsV2Authorization {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
aciCache.clear();
|
||||
pniCache.clear();
|
||||
}
|
||||
|
||||
private static int currentTimeDays() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorization(ACI self,
|
||||
private GroupsV2AuthorizationString getAuthorization(ServiceId authServiceId,
|
||||
GroupSecretParams groupSecretParams,
|
||||
Map<Integer, AuthCredentialResponse> credentials,
|
||||
int today)
|
||||
@@ -77,7 +94,7 @@ public class GroupsV2Authorization {
|
||||
throw new NoCredentialForRedemptionTimeException();
|
||||
}
|
||||
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(self, today, groupSecretParams, authCredentialResponse);
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(authServiceId, today, groupSecretParams, authCredentialResponse);
|
||||
}
|
||||
|
||||
public interface ValueCache {
|
||||
|
||||
@@ -58,7 +58,8 @@ final class AddGroupDetailsRepository {
|
||||
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
|
||||
|
||||
try {
|
||||
GroupManager.GroupActionResult result = GroupManager.createGroup(context,
|
||||
GroupManager.GroupActionResult result = GroupManager.createGroup(SignalStore.account().requireAci(),
|
||||
context,
|
||||
recipients,
|
||||
avatar,
|
||||
name,
|
||||
|
||||
@@ -100,7 +100,7 @@ final class PendingMemberInvitesRepository {
|
||||
@WorkerThread
|
||||
boolean revokeInvites(@NonNull Collection<UuidCiphertext> uuidCipherTexts) {
|
||||
try {
|
||||
GroupManager.revokeInvites(context, groupId, uuidCipherTexts);
|
||||
GroupManager.revokeInvites(context, SignalStore.account().requireAci(), groupId, uuidCipherTexts);
|
||||
return true;
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
@@ -52,7 +53,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException;
|
||||
@@ -67,7 +67,6 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Advances a groups state to a specified revision.
|
||||
@@ -104,11 +103,10 @@ public class GroupsV2StateProcessor {
|
||||
this.groupDatabase = SignalDatabase.groups();
|
||||
}
|
||||
|
||||
public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) {
|
||||
ACI selfAci = SignalStore.account().requireAci();
|
||||
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, selfAci, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
|
||||
public StateProcessorForGroup forGroup(@NonNull ServiceId serviceId, @NonNull GroupMasterKey groupMasterKey) {
|
||||
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceId, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
|
||||
|
||||
return new StateProcessorForGroup(selfAci, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
|
||||
return new StateProcessorForGroup(serviceId, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
|
||||
}
|
||||
|
||||
public enum GroupState {
|
||||
@@ -142,7 +140,7 @@ public class GroupsV2StateProcessor {
|
||||
}
|
||||
|
||||
public static final class StateProcessorForGroup {
|
||||
private final ACI selfAci;
|
||||
private final ServiceId serviceId;
|
||||
private final Context context;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
@@ -152,7 +150,7 @@ public class GroupsV2StateProcessor {
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
private final ProfileAndMessageHelper profileAndMessageHelper;
|
||||
|
||||
@VisibleForTesting StateProcessorForGroup(@NonNull ACI selfAci,
|
||||
@VisibleForTesting StateProcessorForGroup(@NonNull ServiceId serviceId,
|
||||
@NonNull Context context,
|
||||
@NonNull GroupDatabase groupDatabase,
|
||||
@NonNull GroupsV2Api groupsV2Api,
|
||||
@@ -160,7 +158,7 @@ public class GroupsV2StateProcessor {
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull ProfileAndMessageHelper profileAndMessageHelper)
|
||||
{
|
||||
this.selfAci = selfAci;
|
||||
this.serviceId = serviceId;
|
||||
this.context = context;
|
||||
this.groupDatabase = groupDatabase;
|
||||
this.groupsV2Api = groupsV2Api;
|
||||
@@ -197,7 +195,7 @@ public class GroupsV2StateProcessor {
|
||||
revision == signedGroupChange.getRevision())
|
||||
{
|
||||
|
||||
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange)) {
|
||||
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) {
|
||||
Log.w(TAG, "Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch.");
|
||||
} else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
|
||||
Log.w(TAG, "Ignoring P2P group change by setting");
|
||||
@@ -233,8 +231,9 @@ public class GroupsV2StateProcessor {
|
||||
}
|
||||
|
||||
if (inputGroupState == null) {
|
||||
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, selfAci.uuid())) {
|
||||
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceId.uuid())) {
|
||||
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member");
|
||||
throw new GroupNotAMemberException(e, true);
|
||||
} else {
|
||||
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
|
||||
insertGroupLeave();
|
||||
@@ -277,26 +276,34 @@ public class GroupsV2StateProcessor {
|
||||
.map(DecryptedMember::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet())
|
||||
.contains(selfAci.uuid());
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
|
||||
boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList()
|
||||
.stream()
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet())
|
||||
.contains(selfAci.uuid());
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
|
||||
return !currentlyInGroup && !addedAsMember && !addedAsPendingMember;
|
||||
}
|
||||
|
||||
private boolean notHavingInviteRevoked(@NonNull DecryptedGroupChange signedGroupChange) {
|
||||
boolean havingInviteRevoked = signedGroupChange.getDeletePendingMembersList()
|
||||
.stream()
|
||||
.map(DecryptedPendingMemberRemoval::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
|
||||
return !havingInviteRevoked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using network, attempt to bring the local copy of the group up to the revision specified via paging.
|
||||
*/
|
||||
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException {
|
||||
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
||||
ACI selfAci = this.selfAci;
|
||||
|
||||
Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
|
||||
|
||||
@@ -304,37 +311,38 @@ public class GroupsV2StateProcessor {
|
||||
GlobalGroupState inputGroupState;
|
||||
|
||||
try {
|
||||
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
|
||||
} catch (NotInGroupException | GroupNotFoundException e) {
|
||||
throw new GroupNotAMemberException(e);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList())) {
|
||||
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList())) {
|
||||
Log.i(TAG, "Local state is at or later than server");
|
||||
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||
}
|
||||
|
||||
if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) {
|
||||
if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceId.uuid(), latestServerGroup.getMembersList())) {
|
||||
Log.i(TAG, "Latest revision or not a member, use latest only");
|
||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null)));
|
||||
} else {
|
||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid());
|
||||
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
||||
boolean includeFirstState = forceIncludeFirst ||
|
||||
localState == null ||
|
||||
localState.getRevision() < 0 ||
|
||||
localState.getRevision() == revisionWeWereAdded ||
|
||||
!GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList()) ||
|
||||
(revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
|
||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceId.uuid());
|
||||
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
||||
|
||||
boolean includeFirstState = forceIncludeFirst ||
|
||||
localState == null ||
|
||||
localState.getRevision() < 0 ||
|
||||
localState.getRevision() == revisionWeWereAdded ||
|
||||
!GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList()) ||
|
||||
(revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
|
||||
|
||||
Log.i(TAG,
|
||||
"Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") +
|
||||
" logsNeededFrom: " + logsNeededFrom +
|
||||
" includeFirstState: " + includeFirstState +
|
||||
" forceIncludeFirst: " + forceIncludeFirst);
|
||||
inputGroupState = getFullMemberHistoryPage(localState, selfAci, logsNeededFrom, includeFirstState);
|
||||
inputGroupState = getFullMemberHistoryPage(localState, serviceId, logsNeededFrom, includeFirstState);
|
||||
}
|
||||
|
||||
ProfileKeySet profileKeys = new ProfileKeySet();
|
||||
@@ -350,7 +358,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
if (newLocalState != null && !inputGroupState.hasMore() && !forceIncludeFirst) {
|
||||
int newLocalRevision = newLocalState.getRevision();
|
||||
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
|
||||
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
|
||||
if (newLocalRevision < requestRevision) {
|
||||
Log.w(TAG, "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]");
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, true);
|
||||
@@ -382,7 +390,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
if (hasMore) {
|
||||
Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
|
||||
inputGroupState = getFullMemberHistoryPage(finalState, selfAci, inputGroupState.getNextPageRevision(), false);
|
||||
inputGroupState = getFullMemberHistoryPage(finalState, serviceId, inputGroupState.getNextPageRevision(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +414,7 @@ public class GroupsV2StateProcessor {
|
||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
|
||||
} catch (GroupNotFoundException e) {
|
||||
throw new GroupDoesNotExistException(e);
|
||||
} catch (NotInGroupException e) {
|
||||
@@ -421,7 +429,7 @@ public class GroupsV2StateProcessor {
|
||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), true)
|
||||
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), true)
|
||||
.getResults()
|
||||
.get(0)
|
||||
.getGroup()
|
||||
@@ -442,7 +450,7 @@ public class GroupsV2StateProcessor {
|
||||
}
|
||||
|
||||
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||
UUID selfUuid = selfAci.uuid();
|
||||
UUID selfUuid = serviceId.uuid();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
||||
.requireV2GroupProperties()
|
||||
@@ -501,7 +509,7 @@ public class GroupsV2StateProcessor {
|
||||
boolean needsAvatarFetch;
|
||||
|
||||
if (inputGroupState.getLocalState() == null) {
|
||||
groupDatabase.create(masterKey, newLocalState);
|
||||
groupDatabase.create(serviceId, masterKey, newLocalState);
|
||||
needsAvatarFetch = !TextUtils.isEmpty(newLocalState.getAvatar());
|
||||
} else {
|
||||
groupDatabase.update(masterKey, newLocalState);
|
||||
@@ -515,9 +523,9 @@ public class GroupsV2StateProcessor {
|
||||
profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState);
|
||||
}
|
||||
|
||||
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ACI selfAci, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
|
||||
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ServiceId serviceId, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
|
||||
try {
|
||||
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), includeFirstState);
|
||||
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), includeFirstState);
|
||||
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupHistoryPage.getResults().size());
|
||||
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
|
||||
|
||||
@@ -545,14 +553,14 @@ public class GroupsV2StateProcessor {
|
||||
static class ProfileAndMessageHelper {
|
||||
|
||||
private final Context context;
|
||||
private final ACI selfAci;
|
||||
private final ServiceId serviceId;
|
||||
private final GroupMasterKey masterKey;
|
||||
private final GroupId.V2 groupId;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
|
||||
ProfileAndMessageHelper(@NonNull Context context, @NonNull ACI selfAci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
|
||||
ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
|
||||
this.context = context;
|
||||
this.selfAci = selfAci;
|
||||
this.serviceId = serviceId;
|
||||
this.masterKey = masterKey;
|
||||
this.groupId = groupId;
|
||||
this.recipientDatabase = recipientDatabase;
|
||||
@@ -560,15 +568,15 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
void determineProfileSharing(@NonNull GlobalGroupState inputGroupState, @NonNull DecryptedGroup newLocalState) {
|
||||
if (inputGroupState.getLocalState() != null) {
|
||||
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), selfAci.uuid()).isPresent();
|
||||
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), serviceId.uuid()).isPresent();
|
||||
|
||||
if (wasAMemberAlready) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), serviceId.uuid());
|
||||
Optional<DecryptedPendingMember> selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), serviceId.uuid());
|
||||
|
||||
if (selfAsMemberOptional.isPresent()) {
|
||||
DecryptedMember selfAsMember = selfAsMemberOptional.get();
|
||||
@@ -587,7 +595,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId()));
|
||||
|
||||
if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), selfAci.uuid()))) {
|
||||
if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), serviceId.uuid()))) {
|
||||
Log.i(TAG, "Added by a blocked user. Leaving group.");
|
||||
ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId));
|
||||
//noinspection UnnecessaryReturnStatement
|
||||
@@ -671,7 +679,7 @@ public class GroupsV2StateProcessor {
|
||||
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
||||
Optional<ServiceId> editor = getEditor(decryptedGroupV2Context).map(ServiceId::from);
|
||||
|
||||
boolean outgoing = !editor.isPresent() || selfAci.equals(editor.get());
|
||||
boolean outgoing = !editor.isPresent() || serviceId.equals(editor.get());
|
||||
|
||||
if (outgoing) {
|
||||
try {
|
||||
@@ -709,7 +717,7 @@ public class GroupsV2StateProcessor {
|
||||
if (changeEditor.isPresent()) {
|
||||
return changeEditor;
|
||||
} else {
|
||||
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), serviceId.uuid());
|
||||
if (pendingByUuid.isPresent()) {
|
||||
return Optional.ofNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user