Migrate GV1 to GV2 on to server. Allow query of group status.
This commit is contained in:
parent
31e137cf6d
commit
985a220fca
5 changed files with 160 additions and 63 deletions
|
@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -80,6 +79,14 @@ public final class GroupManager {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void migrateGroupToServer(@NonNull Context context,
|
||||
@NonNull GroupId.V1 groupIdV1)
|
||||
throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
|
||||
{
|
||||
new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1);
|
||||
}
|
||||
|
||||
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
|
||||
Set<RecipientId> results = new HashSet<>(recipients.size());
|
||||
|
||||
|
@ -164,6 +171,21 @@ public final class GroupManager {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
|
||||
@NonNull GroupMasterKey groupMasterKey)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
new GroupManagerV2(context).groupServerQuery(groupMasterKey);
|
||||
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
|
||||
} catch (GroupNotAMemberException e) {
|
||||
return V2GroupServerStatus.NOT_A_MEMBER;
|
||||
} catch (GroupDoesNotExistException e) {
|
||||
return V2GroupServerStatus.DOES_NOT_EXIST;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void setMemberAdmin(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
|
@ -303,7 +325,7 @@ public final class GroupManager {
|
|||
} else {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
|
||||
List<RecipientId> members = groupRecord.getMembers();
|
||||
byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null;
|
||||
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
|
||||
Set<RecipientId> recipientIds = new HashSet<>(members);
|
||||
int originalSize = recipientIds.size();
|
||||
|
||||
|
@ -388,4 +410,13 @@ public final class GroupManager {
|
|||
ENABLED,
|
||||
ENABLED_WITH_APPROVAL
|
||||
}
|
||||
|
||||
public enum V2GroupServerStatus {
|
||||
/** The group does not exist. The expected pre-migration state for V1 groups. */
|
||||
DOES_NOT_EXIST,
|
||||
/** Group exists but self is not in the group. */
|
||||
NOT_A_MEMBER,
|
||||
/** Self is a full or pending member of the group. */
|
||||
FULL_OR_PENDING_MEMBER
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,6 +138,31 @@ final class GroupManagerV2 {
|
|||
return new GroupUpdater(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void groupServerQuery(@NonNull GroupMasterKey groupMasterKey)
|
||||
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
|
||||
.getCurrentGroupStateFromServer();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1)
|
||||
throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
|
||||
{
|
||||
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupIdV1);
|
||||
String name = groupRecord.getTitle();
|
||||
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
|
||||
int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpireMessages();
|
||||
Set<RecipientId> memberIds = Stream.of(groupRecord.getMembers())
|
||||
.filterNot(m -> m.equals(Recipient.self().getId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
|
||||
}
|
||||
|
||||
final class GroupCreator extends LockOwner {
|
||||
|
||||
GroupCreator(@NonNull Closeable lock) {
|
||||
|
@ -150,59 +175,43 @@ final class GroupManagerV2 {
|
|||
@Nullable byte[] avatar)
|
||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
||||
{
|
||||
if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) {
|
||||
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||
}
|
||||
return createGroup(name, avatar, members);
|
||||
}
|
||||
|
||||
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
|
||||
|
||||
if (SignalStore.internalValues().gv2ForceInvites()) {
|
||||
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
|
||||
}
|
||||
|
||||
if (!self.hasProfileKeyCredential()) {
|
||||
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
|
||||
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
|
||||
}
|
||||
|
||||
GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(name,
|
||||
Optional.fromNullable(avatar),
|
||||
self,
|
||||
candidates);
|
||||
|
||||
GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
|
||||
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||
@WorkerThread
|
||||
private @NonNull GroupManager.GroupActionResult createGroup(@Nullable String name,
|
||||
@Nullable byte[] avatar,
|
||||
@NonNull Collection<RecipientId> members)
|
||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
||||
{
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
|
||||
DecryptedGroup decryptedGroup;
|
||||
|
||||
try {
|
||||
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||
|
||||
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||
if (decryptedGroup == null) {
|
||||
throw new GroupChangeFailedException();
|
||||
}
|
||||
|
||||
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
|
||||
groupDatabase.onAvatarUpdated(groupId, avatar != null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||
|
||||
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
|
||||
.setEditor(UuidUtil.toByteString(selfUuid))
|
||||
.build();
|
||||
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
|
||||
|
||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
|
||||
recipientAndThread.threadId,
|
||||
decryptedGroup.getMembersCount() - 1,
|
||||
getPendingMemberRecipientIds(decryptedGroup.getPendingMembersList()));
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | GroupExistsException e) {
|
||||
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, 0);
|
||||
} catch (GroupAlreadyExistsException e) {
|
||||
throw new GroupChangeFailedException(e);
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
|
||||
groupDatabase.onAvatarUpdated(groupId, avatar != null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||
|
||||
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
|
||||
.setEditor(UuidUtil.toByteString(selfUuid))
|
||||
.build();
|
||||
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
|
||||
|
||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
|
||||
recipientAndThread.threadId,
|
||||
decryptedGroup.getMembersCount() - 1,
|
||||
getPendingMemberRecipientIds(decryptedGroup.getPendingMembersList()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,7 +238,7 @@ final class GroupManagerV2 {
|
|||
@NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection<RecipientId> newMembers)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception
|
||||
{
|
||||
if (!GroupsV2CapabilityChecker.allSupportGroupsV2AndUuid(newMembers)) {
|
||||
if (!GroupsV2CapabilityChecker.allHaveUuidAndSupportGroupsV2(newMembers)) {
|
||||
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
|
||||
}
|
||||
|
||||
|
@ -586,6 +595,56 @@ final class GroupManagerV2 {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
|
||||
@Nullable String name,
|
||||
@Nullable byte[] avatar,
|
||||
@NonNull Collection<RecipientId> members,
|
||||
@NonNull Member.Role memberRole,
|
||||
int disappearingMessageTimerSeconds)
|
||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
|
||||
{
|
||||
if (!GroupsV2CapabilityChecker.allAndSelfHaveUuidAndSupportGroupsV2(members)) {
|
||||
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 capability or we don't have their UUID");
|
||||
}
|
||||
|
||||
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
|
||||
|
||||
if (SignalStore.internalValues().gv2ForceInvites()) {
|
||||
Log.w(TAG, "Forcing GV2 invites due to internal setting");
|
||||
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
|
||||
}
|
||||
|
||||
if (!self.hasProfileKeyCredential()) {
|
||||
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
|
||||
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
|
||||
}
|
||||
|
||||
GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(groupSecretParams,
|
||||
name,
|
||||
Optional.fromNullable(avatar),
|
||||
self,
|
||||
candidates,
|
||||
memberRole,
|
||||
disappearingMessageTimerSeconds);
|
||||
|
||||
try {
|
||||
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||
|
||||
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
|
||||
if (decryptedGroup == null) {
|
||||
throw new GroupChangeFailedException();
|
||||
}
|
||||
|
||||
return decryptedGroup;
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new GroupChangeFailedException(e);
|
||||
} catch (GroupExistsException e) {
|
||||
throw new GroupAlreadyExistsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
final class GroupJoiner extends LockOwner {
|
||||
private final GroupId.V2 groupId;
|
||||
private final GroupLinkPassword password;
|
||||
|
@ -777,7 +836,7 @@ final class GroupManagerV2 {
|
|||
private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision)
|
||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
|
||||
{
|
||||
if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(Collections.singleton(Recipient.self().getId()))) {
|
||||
if (!GroupsV2CapabilityChecker.allAndSelfHaveUuidAndSupportGroupsV2(Collections.singleton(Recipient.self().getId()))) {
|
||||
throw new MembershipNotSuitableForV2Exception("Self does not support GV2 or UUID capabilities");
|
||||
}
|
||||
|
||||
|
|
|
@ -52,18 +52,18 @@ public final class GroupsV2CapabilityChecker {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
static boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
static boolean allAndSelfHaveUuidAndSupportGroupsV2(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws IOException
|
||||
{
|
||||
HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||
|
||||
recipientIdsSet.add(Recipient.self().getId());
|
||||
|
||||
return allSupportGroupsV2AndUuid(recipientIdsSet);
|
||||
return allHaveUuidAndSupportGroupsV2(recipientIdsSet);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
static boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
|
||||
static boolean allHaveUuidAndSupportGroupsV2(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws IOException
|
||||
{
|
||||
Set<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
|
||||
|
|
|
@ -96,6 +96,11 @@ public class AvatarHelper {
|
|||
return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0);
|
||||
}
|
||||
|
||||
public static byte[] getAvatarBytes(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
|
||||
return hasAvatar(context, recipientId) ? Util.readFully(getAvatar(context, recipientId))
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the avatar on disk.
|
||||
*/
|
||||
|
|
|
@ -79,23 +79,26 @@ public final class GroupsV2Operations {
|
|||
* @param self You will be member 0 and the only admin.
|
||||
* @param members Members must not contain self. Members will be non-admin members of the group.
|
||||
*/
|
||||
public NewGroup createNewGroup(final String title,
|
||||
public NewGroup createNewGroup(final GroupSecretParams groupSecretParams,
|
||||
final String title,
|
||||
final Optional<byte[]> avatar,
|
||||
final GroupCandidate self,
|
||||
final Set<GroupCandidate> members) {
|
||||
final Set<GroupCandidate> members,
|
||||
final Member.Role memberRole,
|
||||
final int disappearingMessageTimerSeconds)
|
||||
{
|
||||
|
||||
if (members.contains(self)) {
|
||||
throw new IllegalArgumentException("Members must not contain self");
|
||||
}
|
||||
|
||||
final GroupSecretParams groupSecretParams = GroupSecretParams.generate(random);
|
||||
final GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||
final GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||
|
||||
Group.Builder group = Group.newBuilder()
|
||||
.setRevision(0)
|
||||
.setPublicKey(ByteString.copyFrom(groupSecretParams.getPublicParams().serialize()))
|
||||
.setTitle(groupOperations.encryptTitle(title))
|
||||
.setDisappearingMessagesTimer(groupOperations.encryptTimer(0))
|
||||
.setDisappearingMessagesTimer(groupOperations.encryptTimer(disappearingMessageTimerSeconds))
|
||||
.setAccessControl(AccessControl.newBuilder()
|
||||
.setAttributes(AccessControl.AccessRequired.MEMBER)
|
||||
.setMembers(AccessControl.AccessRequired.MEMBER));
|
||||
|
@ -103,13 +106,12 @@ public final class GroupsV2Operations {
|
|||
group.addMembers(groupOperations.member(self.getProfileKeyCredential().get(), Member.Role.ADMINISTRATOR));
|
||||
|
||||
for (GroupCandidate credential : members) {
|
||||
Member.Role newMemberRole = Member.Role.DEFAULT;
|
||||
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
|
||||
|
||||
if (profileKeyCredential != null) {
|
||||
group.addMembers(groupOperations.member(profileKeyCredential, newMemberRole));
|
||||
group.addMembers(groupOperations.member(profileKeyCredential, memberRole));
|
||||
} else {
|
||||
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), newMemberRole));
|
||||
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), memberRole));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue