kopia lustrzana https://github.com/ryukoposting/Signal-Android
Improve GV2 update speed by only requesting a full snapshot when necessary.
rodzic
210bb23aa4
commit
bb1e6ffae0
|
@ -58,7 +58,7 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class GroupDatabase extends Database {
|
public class GroupDatabase extends Database {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(GroupDatabase.class);
|
private static final String TAG = Log.tag(GroupDatabase.class);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class GroupsV2Authorization {
|
public class GroupsV2Authorization {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(GroupsV2Authorization.class);
|
private static final String TAG = Log.tag(GroupsV2Authorization.class);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
@ -33,7 +34,6 @@ import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
|
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
|
@ -61,7 +60,6 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -90,7 +88,6 @@ public final class GroupsV2StateProcessor {
|
||||||
public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper.RESTORE_PLACEHOLDER_REVISION;
|
public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper.RESTORE_PLACEHOLDER_REVISION;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final JobManager jobManager;
|
|
||||||
private final RecipientDatabase recipientDatabase;
|
private final RecipientDatabase recipientDatabase;
|
||||||
private final GroupDatabase groupDatabase;
|
private final GroupDatabase groupDatabase;
|
||||||
private final GroupsV2Authorization groupsV2Authorization;
|
private final GroupsV2Authorization groupsV2Authorization;
|
||||||
|
@ -98,7 +95,6 @@ public final class GroupsV2StateProcessor {
|
||||||
|
|
||||||
public GroupsV2StateProcessor(@NonNull Context context) {
|
public GroupsV2StateProcessor(@NonNull Context context) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.jobManager = ApplicationDependencies.getJobManager();
|
|
||||||
this.groupsV2Authorization = ApplicationDependencies.getGroupsV2Authorization();
|
this.groupsV2Authorization = ApplicationDependencies.getGroupsV2Authorization();
|
||||||
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
|
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
|
||||||
this.recipientDatabase = SignalDatabase.recipients();
|
this.recipientDatabase = SignalDatabase.recipients();
|
||||||
|
@ -106,7 +102,10 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) {
|
public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) {
|
||||||
return new StateProcessorForGroup(groupMasterKey);
|
ACI selfAci = Recipient.self().requireAci();
|
||||||
|
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, selfAci, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
|
||||||
|
|
||||||
|
return new StateProcessorForGroup(selfAci, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GroupState {
|
public enum GroupState {
|
||||||
|
@ -128,7 +127,7 @@ public final class GroupsV2StateProcessor {
|
||||||
|
|
||||||
public static class GroupUpdateResult {
|
public static class GroupUpdateResult {
|
||||||
private final GroupState groupState;
|
private final GroupState groupState;
|
||||||
@Nullable private final DecryptedGroup latestServer;
|
private final DecryptedGroup latestServer;
|
||||||
|
|
||||||
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
|
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
|
||||||
this.groupState = groupState;
|
this.groupState = groupState;
|
||||||
|
@ -144,15 +143,34 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class StateProcessorForGroup {
|
public static final class StateProcessorForGroup {
|
||||||
|
private final ACI selfAci;
|
||||||
|
private final Context context;
|
||||||
|
private final GroupDatabase groupDatabase;
|
||||||
|
private final GroupsV2Api groupsV2Api;
|
||||||
|
private final GroupsV2Authorization groupsV2Authorization;
|
||||||
private final GroupMasterKey masterKey;
|
private final GroupMasterKey masterKey;
|
||||||
private final GroupId.V2 groupId;
|
private final GroupId.V2 groupId;
|
||||||
private final GroupSecretParams groupSecretParams;
|
private final GroupSecretParams groupSecretParams;
|
||||||
|
private final ProfileAndMessageHelper profileAndMessageHelper;
|
||||||
|
|
||||||
private StateProcessorForGroup(@NonNull GroupMasterKey groupMasterKey) {
|
@VisibleForTesting StateProcessorForGroup(@NonNull ACI selfAci,
|
||||||
|
@NonNull Context context,
|
||||||
|
@NonNull GroupDatabase groupDatabase,
|
||||||
|
@NonNull GroupsV2Api groupsV2Api,
|
||||||
|
@NonNull GroupsV2Authorization groupsV2Authorization,
|
||||||
|
@NonNull GroupMasterKey groupMasterKey,
|
||||||
|
@NonNull ProfileAndMessageHelper profileAndMessageHelper)
|
||||||
|
{
|
||||||
|
this.selfAci = selfAci;
|
||||||
|
this.context = context;
|
||||||
|
this.groupDatabase = groupDatabase;
|
||||||
|
this.groupsV2Api = groupsV2Api;
|
||||||
|
this.groupsV2Authorization = groupsV2Authorization;
|
||||||
this.masterKey = groupMasterKey;
|
this.masterKey = groupMasterKey;
|
||||||
this.groupId = GroupId.v2(masterKey);
|
this.groupId = GroupId.v2(masterKey);
|
||||||
this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
this.groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||||
|
this.profileAndMessageHelper = profileAndMessageHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -217,7 +235,7 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputGroupState == null) {
|
if (inputGroupState == null) {
|
||||||
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, Recipient.self().requireAci().uuid())) {
|
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, selfAci.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");
|
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");
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
|
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
|
||||||
|
@ -226,8 +244,6 @@ public final class GroupsV2StateProcessor {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Saved server query for group change");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
|
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
|
||||||
|
@ -240,11 +256,11 @@ public final class GroupsV2StateProcessor {
|
||||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||||
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||||
Log.i(TAG, "Inserting single update message for restore placeholder");
|
Log.i(TAG, "Inserting single update message for restore placeholder");
|
||||||
insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||||
} else {
|
} else {
|
||||||
insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||||
}
|
}
|
||||||
persistLearnedProfileKeys(inputGroupState);
|
profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState);
|
||||||
|
|
||||||
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
||||||
if (remainingWork.getServerHistory().size() > 0) {
|
if (remainingWork.getServerHistory().size() > 0) {
|
||||||
|
@ -257,20 +273,22 @@ public final class GroupsV2StateProcessor {
|
||||||
|
|
||||||
private boolean notInGroupAndNotBeingAdded(@NonNull Optional<GroupRecord> localRecord, @NonNull DecryptedGroupChange signedGroupChange) {
|
private boolean notInGroupAndNotBeingAdded(@NonNull Optional<GroupRecord> localRecord, @NonNull DecryptedGroupChange signedGroupChange) {
|
||||||
boolean currentlyInGroup = localRecord.isPresent() && localRecord.get().isActive();
|
boolean currentlyInGroup = localRecord.isPresent() && localRecord.get().isActive();
|
||||||
|
|
||||||
boolean addedAsMember = signedGroupChange.getNewMembersList()
|
boolean addedAsMember = signedGroupChange.getNewMembersList()
|
||||||
.stream()
|
.stream()
|
||||||
.map(DecryptedMember::getUuid)
|
.map(DecryptedMember::getUuid)
|
||||||
.map(UuidUtil::fromByteStringOrNull)
|
.map(UuidUtil::fromByteStringOrNull)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toSet())
|
.collect(Collectors.toSet())
|
||||||
.contains(Recipient.self().requireAci().uuid());
|
.contains(selfAci.uuid());
|
||||||
|
|
||||||
boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList()
|
boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList()
|
||||||
.stream()
|
.stream()
|
||||||
.map(DecryptedPendingMember::getUuid)
|
.map(DecryptedPendingMember::getUuid)
|
||||||
.map(UuidUtil::fromByteStringOrNull)
|
.map(UuidUtil::fromByteStringOrNull)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toSet())
|
.collect(Collectors.toSet())
|
||||||
.contains(Recipient.self().requireAci().uuid());
|
.contains(selfAci.uuid());
|
||||||
|
|
||||||
return !currentlyInGroup && !addedAsMember && !addedAsPendingMember;
|
return !currentlyInGroup && !addedAsMember && !addedAsPendingMember;
|
||||||
}
|
}
|
||||||
|
@ -280,9 +298,9 @@ public final class GroupsV2StateProcessor {
|
||||||
*/
|
*/
|
||||||
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp) throws IOException, GroupNotAMemberException {
|
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp) throws IOException, GroupNotAMemberException {
|
||||||
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
||||||
ACI selfAci = Recipient.self().requireAci();
|
ACI selfAci = this.selfAci;
|
||||||
|
|
||||||
Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + " latest only: " + latestRevisionOnly);
|
Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
|
||||||
|
|
||||||
DecryptedGroup latestServerGroup;
|
DecryptedGroup latestServerGroup;
|
||||||
GlobalGroupState inputGroupState;
|
GlobalGroupState inputGroupState;
|
||||||
|
@ -295,15 +313,21 @@ public final class GroupsV2StateProcessor {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision()) {
|
||||||
|
Log.i(TAG, "Local state is at or later than server");
|
||||||
|
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, latestServerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) {
|
if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) {
|
||||||
Log.i(TAG, "Latest revision or not a member, use latest only");
|
Log.i(TAG, "Latest revision or not a member, use latest only");
|
||||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup, null)));
|
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup, null)));
|
||||||
} else {
|
} else {
|
||||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid());
|
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid());
|
||||||
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
||||||
|
boolean includeFirstState = localState == null || localState.getRevision() < 0 || (revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
|
||||||
|
|
||||||
Log.i(TAG, "Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") + " logsNeededFrom: " + logsNeededFrom);
|
Log.i(TAG, "Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") + " logsNeededFrom: " + logsNeededFrom);
|
||||||
inputGroupState = getFullMemberHistoryPage(localState, selfAci, logsNeededFrom);
|
inputGroupState = getFullMemberHistoryPage(localState, selfAci, logsNeededFrom, includeFirstState);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileKeySet profileKeys = new ProfileKeySet();
|
ProfileKeySet profileKeys = new ProfileKeySet();
|
||||||
|
@ -324,7 +348,7 @@ public final class GroupsV2StateProcessor {
|
||||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||||
|
|
||||||
if (localState == null || localState.getRevision() != GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
if (localState == null || localState.getRevision() != GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||||
timestamp = insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
timestamp = profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ServerGroupLogEntry entry : inputGroupState.getServerHistory()) {
|
for (ServerGroupLogEntry entry : inputGroupState.getServerHistory()) {
|
||||||
|
@ -342,16 +366,16 @@ public final class GroupsV2StateProcessor {
|
||||||
|
|
||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
|
Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
|
||||||
inputGroupState = getFullMemberHistoryPage(finalState, selfAci, inputGroupState.getNextPageRevision());
|
inputGroupState = getFullMemberHistoryPage(finalState, selfAci, inputGroupState.getNextPageRevision(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||||
Log.i(TAG, "Inserting single update message for restore placeholder");
|
Log.i(TAG, "Inserting single update message for restore placeholder");
|
||||||
insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null)));
|
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
persistLearnedProfileKeys(profileKeys);
|
profileAndMessageHelper.persistLearnedProfileKeys(profileKeys);
|
||||||
|
|
||||||
if (finalGlobalGroupState.getServerHistory().size() > 0) {
|
if (finalGlobalGroupState.getServerHistory().size() > 0) {
|
||||||
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber()));
|
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber()));
|
||||||
|
@ -366,7 +390,7 @@ public final class GroupsV2StateProcessor {
|
||||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(Recipient.self().requireAci(), groupSecretParams));
|
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||||
} catch (GroupNotFoundException e) {
|
} catch (GroupNotFoundException e) {
|
||||||
throw new GroupDoesNotExistException(e);
|
throw new GroupDoesNotExistException(e);
|
||||||
} catch (NotInGroupException e) {
|
} catch (NotInGroupException e) {
|
||||||
|
@ -381,7 +405,7 @@ public final class GroupsV2StateProcessor {
|
||||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(Recipient.self().requireAci(), groupSecretParams))
|
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), true)
|
||||||
.getResults()
|
.getResults()
|
||||||
.get(0)
|
.get(0)
|
||||||
.getGroup()
|
.getGroup()
|
||||||
|
@ -402,12 +426,14 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
|
||||||
UUID selfUuid = Recipient.self().requireAci().uuid();
|
UUID selfUuid = selfAci.uuid();
|
||||||
|
|
||||||
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
||||||
.requireV2GroupProperties()
|
.requireV2GroupProperties()
|
||||||
.getDecryptedGroup();
|
.getDecryptedGroup();
|
||||||
|
|
||||||
DecryptedGroup simulatedGroupState = DecryptedGroupUtil.removeMember(decryptedGroup, selfUuid, decryptedGroup.getRevision() + 1);
|
DecryptedGroup simulatedGroupState = DecryptedGroupUtil.removeMember(decryptedGroup, selfUuid, decryptedGroup.getRevision() + 1);
|
||||||
|
|
||||||
DecryptedGroupChange simulatedGroupChange = DecryptedGroupChange.newBuilder()
|
DecryptedGroupChange simulatedGroupChange = DecryptedGroupChange.newBuilder()
|
||||||
.setEditor(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID))
|
.setEditor(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID))
|
||||||
.setRevision(simulatedGroupState.getRevision())
|
.setRevision(simulatedGroupState.getRevision())
|
||||||
|
@ -415,6 +441,7 @@ public final class GroupsV2StateProcessor {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null);
|
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(decryptedGroup, simulatedGroupChange, simulatedGroupState), null);
|
||||||
|
|
||||||
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient,
|
||||||
decryptedGroupV2Context,
|
decryptedGroupV2Context,
|
||||||
null,
|
null,
|
||||||
|
@ -466,25 +493,65 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsAvatarFetch) {
|
if (needsAvatarFetch) {
|
||||||
jobManager.add(new AvatarGroupsV2DownloadJob(groupId, newLocalState.getAvatar()));
|
ApplicationDependencies.getJobManager().add(new AvatarGroupsV2DownloadJob(groupId, newLocalState.getAvatar()));
|
||||||
}
|
}
|
||||||
|
|
||||||
determineProfileSharing(inputGroupState, newLocalState);
|
profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void determineProfileSharing(@NonNull GlobalGroupState inputGroupState,
|
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ACI selfAci, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
|
||||||
@NonNull DecryptedGroup newLocalState)
|
try {
|
||||||
{
|
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), includeFirstState);
|
||||||
|
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupHistoryPage.getResults().size());
|
||||||
|
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
|
||||||
|
|
||||||
|
if (ignoreServerChanges) {
|
||||||
|
Log.w(TAG, "Server change logs are ignored by setting");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DecryptedGroupHistoryEntry entry : groupHistoryPage.getResults()) {
|
||||||
|
DecryptedGroup group = entry.getGroup().orNull();
|
||||||
|
DecryptedGroupChange change = ignoreServerChanges ? null : entry.getChange().orNull();
|
||||||
|
|
||||||
|
if (group != null || change != null) {
|
||||||
|
history.add(new ServerGroupLogEntry(group, change));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GlobalGroupState(localState, history, groupHistoryPage.getPagingData());
|
||||||
|
} catch (InvalidGroupStateException | VerificationFailedException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static class ProfileAndMessageHelper {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final ACI aci;
|
||||||
|
private final GroupMasterKey masterKey;
|
||||||
|
private final GroupId.V2 groupId;
|
||||||
|
private final RecipientDatabase recipientDatabase;
|
||||||
|
|
||||||
|
ProfileAndMessageHelper(@NonNull Context context, @NonNull ACI aci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
|
||||||
|
this.context = context;
|
||||||
|
this.aci = aci;
|
||||||
|
this.masterKey = masterKey;
|
||||||
|
this.groupId = groupId;
|
||||||
|
this.recipientDatabase = recipientDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
void determineProfileSharing(@NonNull GlobalGroupState inputGroupState, @NonNull DecryptedGroup newLocalState) {
|
||||||
if (inputGroupState.getLocalState() != null) {
|
if (inputGroupState.getLocalState() != null) {
|
||||||
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), Recipient.self().requireAci().uuid()).isPresent();
|
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), aci.uuid()).isPresent();
|
||||||
|
|
||||||
if (wasAMemberAlready) {
|
if (wasAMemberAlready) {
|
||||||
Log.i(TAG, "Skipping profile sharing detection as was already a full member before update");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), Recipient.self().requireAci().uuid());
|
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), aci.uuid());
|
||||||
|
|
||||||
if (selfAsMemberOptional.isPresent()) {
|
if (selfAsMemberOptional.isPresent()) {
|
||||||
DecryptedMember selfAsMember = selfAsMemberOptional.get();
|
DecryptedMember selfAsMember = selfAsMemberOptional.get();
|
||||||
|
@ -518,7 +585,7 @@ public final class GroupsV2StateProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long insertUpdateMessages(long timestamp,
|
long insertUpdateMessages(long timestamp,
|
||||||
@Nullable DecryptedGroup previousGroupState,
|
@Nullable DecryptedGroup previousGroupState,
|
||||||
Collection<LocalGroupLogEntry> processedLogEntries)
|
Collection<LocalGroupLogEntry> processedLogEntries)
|
||||||
{
|
{
|
||||||
|
@ -538,7 +605,7 @@ public final class GroupsV2StateProcessor {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
|
void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
|
||||||
final ProfileKeySet profileKeys = new ProfileKeySet();
|
final ProfileKeySet profileKeys = new ProfileKeySet();
|
||||||
|
|
||||||
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
|
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
|
||||||
|
@ -553,47 +620,22 @@ public final class GroupsV2StateProcessor {
|
||||||
persistLearnedProfileKeys(profileKeys);
|
persistLearnedProfileKeys(profileKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistLearnedProfileKeys(@NonNull ProfileKeySet profileKeys) {
|
void persistLearnedProfileKeys(@NonNull ProfileKeySet profileKeys) {
|
||||||
Set<RecipientId> updated = recipientDatabase.persistProfileKeySet(profileKeys);
|
Set<RecipientId> updated = recipientDatabase.persistProfileKeySet(profileKeys);
|
||||||
|
|
||||||
if (!updated.isEmpty()) {
|
if (!updated.isEmpty()) {
|
||||||
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, fetching profiles", updated.size()));
|
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, fetching profiles", updated.size()));
|
||||||
|
|
||||||
for (Job job : RetrieveProfileJob.forRecipients(updated)) {
|
for (Job job : RetrieveProfileJob.forRecipients(updated)) {
|
||||||
jobManager.runSynchronously(job, 5000);
|
ApplicationDependencies.getJobManager().runSynchronously(job, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ACI selfAci, int logsNeededFromRevision) throws IOException {
|
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
||||||
try {
|
|
||||||
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
|
||||||
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupHistoryPage.getResults().size());
|
|
||||||
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
|
|
||||||
|
|
||||||
if (ignoreServerChanges) {
|
|
||||||
Log.w(TAG, "Server change logs are ignored by setting");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DecryptedGroupHistoryEntry entry : groupHistoryPage.getResults()) {
|
|
||||||
DecryptedGroup group = entry.getGroup().orNull();
|
|
||||||
DecryptedGroupChange change = ignoreServerChanges ? null : entry.getChange().orNull();
|
|
||||||
|
|
||||||
if (group != null || change != null) {
|
|
||||||
history.add(new ServerGroupLogEntry(group, change));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GlobalGroupState(localState, history, groupHistoryPage.getPagingData());
|
|
||||||
} catch (InvalidGroupStateException | VerificationFailedException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
|
||||||
Optional<ACI> editor = getEditor(decryptedGroupV2Context).transform(ACI::from);
|
Optional<ACI> editor = getEditor(decryptedGroupV2Context).transform(ACI::from);
|
||||||
|
|
||||||
boolean outgoing = !editor.isPresent() || Recipient.self().requireAci().equals(editor.get());
|
boolean outgoing = !editor.isPresent() || aci.equals(editor.get());
|
||||||
|
|
||||||
if (outgoing) {
|
if (outgoing) {
|
||||||
try {
|
try {
|
||||||
|
@ -631,7 +673,7 @@ public final class GroupsV2StateProcessor {
|
||||||
if (changeEditor.isPresent()) {
|
if (changeEditor.isPresent()) {
|
||||||
return changeEditor;
|
return changeEditor;
|
||||||
} else {
|
} else {
|
||||||
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), Recipient.self().requireAci().uuid());
|
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), aci.uuid());
|
||||||
if (pendingByUuid.isPresent()) {
|
if (pendingByUuid.isPresent()) {
|
||||||
return Optional.fromNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid()));
|
return Optional.fromNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,204 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import org.signal.storageservice.protos.groups.AccessControl
|
||||||
|
import org.signal.storageservice.protos.groups.Member
|
||||||
|
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.DecryptedRequestingMember
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedString
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
||||||
|
import org.signal.storageservice.protos.groups.local.EnabledState
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
|
||||||
|
import org.whispersystems.signalservice.api.push.ACI
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
|
|
||||||
|
fun DecryptedGroupChange.Builder.setNewDescription(description: String) {
|
||||||
|
newDescription = DecryptedString.newBuilder().setValue(description).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DecryptedGroupChange.Builder.setNewTitle(title: String) {
|
||||||
|
newTitle = DecryptedString.newBuilder().setValue(title).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeLog(private val revision: Int) {
|
||||||
|
var groupSnapshot: DecryptedGroup? = null
|
||||||
|
var groupChange: DecryptedGroupChange? = null
|
||||||
|
|
||||||
|
fun change(init: DecryptedGroupChange.Builder.() -> Unit) {
|
||||||
|
val builder = DecryptedGroupChange.newBuilder().setRevision(revision)
|
||||||
|
builder.init()
|
||||||
|
groupChange = builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fullSnapshot(
|
||||||
|
extendGroup: DecryptedGroup? = null,
|
||||||
|
title: String = extendGroup?.title ?: "",
|
||||||
|
avatar: String = extendGroup?.avatar ?: "",
|
||||||
|
description: String = extendGroup?.description ?: "",
|
||||||
|
accessControl: AccessControl = extendGroup?.accessControl ?: AccessControl.getDefaultInstance(),
|
||||||
|
members: List<DecryptedMember> = extendGroup?.membersList ?: emptyList(),
|
||||||
|
pendingMembers: List<DecryptedPendingMember> = extendGroup?.pendingMembersList ?: emptyList(),
|
||||||
|
requestingMembers: List<DecryptedRequestingMember> = extendGroup?.requestingMembersList ?: emptyList(),
|
||||||
|
inviteLinkPassword: ByteArray = extendGroup?.inviteLinkPassword?.toByteArray() ?: ByteArray(0),
|
||||||
|
disappearingMessageTimer: DecryptedTimer = extendGroup?.disappearingMessagesTimer ?: DecryptedTimer.getDefaultInstance()
|
||||||
|
) {
|
||||||
|
groupSnapshot = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeSet {
|
||||||
|
private val changeSet: MutableList<ChangeLog> = mutableListOf()
|
||||||
|
|
||||||
|
fun changeLog(revision: Int, init: ChangeLog.() -> Unit) {
|
||||||
|
val entry = ChangeLog(revision)
|
||||||
|
entry.init()
|
||||||
|
changeSet += entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toApiResponse(): GroupHistoryPage {
|
||||||
|
return GroupHistoryPage(changeSet.map { DecryptedGroupHistoryEntry(Optional.fromNullable(it.groupSnapshot), Optional.fromNullable(it.groupChange)) }, GroupHistoryPage.PagingData.NONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupStateTestData(private val masterKey: GroupMasterKey) {
|
||||||
|
|
||||||
|
var localState: DecryptedGroup? = null
|
||||||
|
var groupRecord: Optional<GroupDatabase.GroupRecord> = Optional.absent()
|
||||||
|
var serverState: DecryptedGroup? = null
|
||||||
|
var changeSet: ChangeSet? = null
|
||||||
|
var includeFirst: Boolean = false
|
||||||
|
var requestedRevision: Int = 0
|
||||||
|
|
||||||
|
fun localState(
|
||||||
|
revision: Int = 0,
|
||||||
|
title: String = "",
|
||||||
|
avatar: String = "",
|
||||||
|
description: String = "",
|
||||||
|
accessControl: AccessControl = AccessControl.getDefaultInstance(),
|
||||||
|
members: List<DecryptedMember> = emptyList(),
|
||||||
|
pendingMembers: List<DecryptedPendingMember> = emptyList(),
|
||||||
|
requestingMembers: List<DecryptedRequestingMember> = emptyList(),
|
||||||
|
inviteLinkPassword: ByteArray = ByteArray(0),
|
||||||
|
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance()
|
||||||
|
) {
|
||||||
|
localState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer)
|
||||||
|
groupRecord = groupRecord(masterKey, localState!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serverState(
|
||||||
|
revision: Int,
|
||||||
|
extendGroup: DecryptedGroup? = null,
|
||||||
|
title: String = extendGroup?.title ?: "",
|
||||||
|
avatar: String = extendGroup?.avatar ?: "",
|
||||||
|
description: String = extendGroup?.description ?: "",
|
||||||
|
accessControl: AccessControl = extendGroup?.accessControl ?: AccessControl.getDefaultInstance(),
|
||||||
|
members: List<DecryptedMember> = extendGroup?.membersList ?: emptyList(),
|
||||||
|
pendingMembers: List<DecryptedPendingMember> = extendGroup?.pendingMembersList ?: emptyList(),
|
||||||
|
requestingMembers: List<DecryptedRequestingMember> = extendGroup?.requestingMembersList ?: emptyList(),
|
||||||
|
inviteLinkPassword: ByteArray = extendGroup?.inviteLinkPassword?.toByteArray() ?: ByteArray(0),
|
||||||
|
disappearingMessageTimer: DecryptedTimer = extendGroup?.disappearingMessagesTimer ?: DecryptedTimer.getDefaultInstance()
|
||||||
|
) {
|
||||||
|
serverState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeSet(init: ChangeSet.() -> Unit) {
|
||||||
|
val changeSet = ChangeSet()
|
||||||
|
changeSet.init()
|
||||||
|
this.changeSet = changeSet
|
||||||
|
}
|
||||||
|
|
||||||
|
fun apiCallParameters(requestedRevision: Int, includeFirst: Boolean) {
|
||||||
|
this.requestedRevision = requestedRevision
|
||||||
|
this.includeFirst = includeFirst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupRecord(
|
||||||
|
masterKey: GroupMasterKey,
|
||||||
|
decryptedGroup: DecryptedGroup,
|
||||||
|
id: GroupId = GroupId.v2(masterKey),
|
||||||
|
recipientId: RecipientId = RecipientId.from(100),
|
||||||
|
members: String = "1",
|
||||||
|
unmigratedV1Members: String? = null,
|
||||||
|
avatarId: Long = 1,
|
||||||
|
avatarKey: ByteArray = ByteArray(0),
|
||||||
|
avatarContentType: String = "",
|
||||||
|
relay: String = "",
|
||||||
|
active: Boolean = true,
|
||||||
|
avatarDigest: ByteArray = ByteArray(0),
|
||||||
|
mms: Boolean = false,
|
||||||
|
distributionId: DistributionId? = null
|
||||||
|
): Optional<GroupDatabase.GroupRecord> {
|
||||||
|
return Optional.of(
|
||||||
|
GroupDatabase.GroupRecord(
|
||||||
|
id,
|
||||||
|
recipientId,
|
||||||
|
decryptedGroup.title,
|
||||||
|
members,
|
||||||
|
unmigratedV1Members,
|
||||||
|
avatarId,
|
||||||
|
avatarKey,
|
||||||
|
avatarContentType,
|
||||||
|
relay,
|
||||||
|
active,
|
||||||
|
avatarDigest,
|
||||||
|
mms,
|
||||||
|
masterKey.serialize(),
|
||||||
|
decryptedGroup.revision,
|
||||||
|
decryptedGroup.toByteArray(),
|
||||||
|
distributionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptedGroup(
|
||||||
|
revision: Int = 0,
|
||||||
|
title: String = "",
|
||||||
|
avatar: String = "",
|
||||||
|
description: String = "",
|
||||||
|
accessControl: AccessControl = AccessControl.getDefaultInstance(),
|
||||||
|
members: List<DecryptedMember> = emptyList(),
|
||||||
|
pendingMembers: List<DecryptedPendingMember> = emptyList(),
|
||||||
|
requestingMembers: List<DecryptedRequestingMember> = emptyList(),
|
||||||
|
inviteLinkPassword: ByteArray = ByteArray(0),
|
||||||
|
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance()
|
||||||
|
): DecryptedGroup {
|
||||||
|
|
||||||
|
val builder = DecryptedGroup.newBuilder()
|
||||||
|
.setAccessControl(accessControl)
|
||||||
|
.setAvatar(avatar)
|
||||||
|
.setAvatarBytes(ByteString.EMPTY)
|
||||||
|
.setDescription(description)
|
||||||
|
.setDisappearingMessagesTimer(disappearingMessageTimer)
|
||||||
|
.setInviteLinkPassword(ByteString.copyFrom(inviteLinkPassword))
|
||||||
|
.setIsAnnouncementGroup(EnabledState.DISABLED)
|
||||||
|
.setTitle(title)
|
||||||
|
.setRevision(revision)
|
||||||
|
.addAllMembers(members)
|
||||||
|
.addAllPendingMembers(pendingMembers)
|
||||||
|
.addAllRequestingMembers(requestingMembers)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun member(aci: ACI, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0): DecryptedMember {
|
||||||
|
return DecryptedMember.newBuilder()
|
||||||
|
.setRole(role)
|
||||||
|
.setUuid(aci.toByteString())
|
||||||
|
.setJoinedAtRevision(joinedAt)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestingMember(aci: ACI): DecryptedRequestingMember {
|
||||||
|
return DecryptedRequestingMember.newBuilder()
|
||||||
|
.setUuid(aci.toByteString())
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -83,7 +83,7 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull JobManager provideJobManager() {
|
public @NonNull JobManager provideJobManager() {
|
||||||
return null;
|
return mock(JobManager.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,375 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.v2.processing
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers.`is`
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentMatchers.eq
|
||||||
|
import org.mockito.Mockito.any
|
||||||
|
import org.mockito.Mockito.doReturn
|
||||||
|
import org.mockito.Mockito.isA
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.reset
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedTimer
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey
|
||||||
|
import org.thoughtcrime.securesms.SignalStoreRule
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.member
|
||||||
|
import org.thoughtcrime.securesms.database.requestingMember
|
||||||
|
import org.thoughtcrime.securesms.database.setNewDescription
|
||||||
|
import org.thoughtcrime.securesms.database.setNewTitle
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupsV2Authorization
|
||||||
|
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||||
|
import org.thoughtcrime.securesms.util.Hex.fromStringCondensed
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
|
||||||
|
import org.whispersystems.signalservice.api.push.ACI
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(manifest = Config.NONE, application = Application::class)
|
||||||
|
class GroupsV2StateProcessorTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||||
|
val selfAci = ACI.from(UUID.randomUUID())
|
||||||
|
val otherAci = ACI.from(UUID.randomUUID())
|
||||||
|
val selfAndOthers = listOf(member(selfAci), member(otherAci))
|
||||||
|
val others = listOf(member(otherAci))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var groupDatabase: GroupDatabase
|
||||||
|
private lateinit var recipientDatabase: RecipientDatabase
|
||||||
|
private lateinit var groupsV2API: GroupsV2Api
|
||||||
|
private lateinit var groupsV2Authorization: GroupsV2Authorization
|
||||||
|
private lateinit var profileAndMessageHelper: GroupsV2StateProcessor.ProfileAndMessageHelper
|
||||||
|
|
||||||
|
private lateinit var processor: GroupsV2StateProcessor.StateProcessorForGroup
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val signalStore: SignalStoreRule = SignalStoreRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
groupDatabase = mock(GroupDatabase::class.java)
|
||||||
|
recipientDatabase = mock(RecipientDatabase::class.java)
|
||||||
|
groupsV2API = mock(GroupsV2Api::class.java)
|
||||||
|
groupsV2Authorization = mock(GroupsV2Authorization::class.java)
|
||||||
|
profileAndMessageHelper = mock(GroupsV2StateProcessor.ProfileAndMessageHelper::class.java)
|
||||||
|
|
||||||
|
processor = GroupsV2StateProcessor.StateProcessorForGroup(selfAci, ApplicationProvider.getApplicationContext(), groupDatabase, groupsV2API, groupsV2Authorization, masterKey, profileAndMessageHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
reset(ApplicationDependencies.getJobManager())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun given(init: GroupStateTestData.() -> Unit) {
|
||||||
|
val data = GroupStateTestData(masterKey)
|
||||||
|
data.init()
|
||||||
|
|
||||||
|
doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java))
|
||||||
|
doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any())
|
||||||
|
|
||||||
|
if (data.serverState != null) {
|
||||||
|
doReturn(data.serverState).`when`(groupsV2API).getGroup(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
data.changeSet?.let { changeSet ->
|
||||||
|
doReturn(changeSet.toApiResponse()).`when`(groupsV2API).getGroupHistoryPage(any(), eq(data.requestedRevision), any(), eq(data.includeFirst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when local revision matches server revision, then return consistent or ahead`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 5,
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 5,
|
||||||
|
extendGroup = localState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
assertThat("local and server match revisions", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when local revision is one less than latest server version, then update from server with group change only`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 5,
|
||||||
|
title = "Fdsa",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 6,
|
||||||
|
extendGroup = localState,
|
||||||
|
title = "Asdf"
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(6) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Asdf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(requestedRevision = 5, includeFirst = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("title changed to match server", result.latestServer!!.title, `is`("Asdf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when local revision is two less than server revision, then update from server with full group state and change`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 5,
|
||||||
|
title = "Fdsa",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 7,
|
||||||
|
title = "Asdf!"
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(6) {
|
||||||
|
fullSnapshot(extendGroup = localState, title = "Asdf")
|
||||||
|
change {
|
||||||
|
setNewTitle("Asdf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeLog(7) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Asdf!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(requestedRevision = 5, includeFirst = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(7))
|
||||||
|
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("Asdf!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when change log returns a group state more than one higher than our local state, then still update to server state`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 100,
|
||||||
|
title = "To infinity",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 111,
|
||||||
|
title = "And beyond",
|
||||||
|
description = "Description"
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(110) {
|
||||||
|
fullSnapshot(
|
||||||
|
extendGroup = localState,
|
||||||
|
title = "And beyond"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
changeLog(111) {
|
||||||
|
change {
|
||||||
|
setNewDescription("Description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(111))
|
||||||
|
assertThat("title changed on server to final result", result.latestServer!!.title, `is`("And beyond"))
|
||||||
|
assertThat("Description updated in change after full snapshot", result.latestServer!!.description, `is`("Description"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when receiving peer change for next revision, then apply change without server call`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = 5,
|
||||||
|
disappearingMessageTimer = DecryptedTimer.newBuilder().setDuration(1000).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val signedChange = DecryptedGroupChange.newBuilder().apply {
|
||||||
|
revision = 6
|
||||||
|
setNewTimer(DecryptedTimer.newBuilder().setDuration(5000))
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(6, 0, signedChange.build())
|
||||||
|
assertThat("revision matches peer change", result.latestServer!!.revision, `is`(6))
|
||||||
|
assertThat("timer changed by peer change", result.latestServer!!.disappearingMessagesTimer.duration, `is`(5000))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when freshly added to a group, with no group changes after being added, then update from server at the revision we were added`() {
|
||||||
|
given {
|
||||||
|
serverState(
|
||||||
|
revision = 2,
|
||||||
|
title = "Breaking Signal for Science",
|
||||||
|
description = "We break stuff, because we must.",
|
||||||
|
members = listOf(member(otherAci), member(selfAci, joinedAt = 2))
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(2) {
|
||||||
|
fullSnapshot(serverState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange.getDefaultInstance())
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when freshly added to a group, with additional group changes after being added, then only update from server at the revision we were added, and then schedule pulling additional changes later`() {
|
||||||
|
given {
|
||||||
|
serverState(
|
||||||
|
revision = 3,
|
||||||
|
title = "Breaking Signal for Science",
|
||||||
|
description = "We break stuff, because we must.",
|
||||||
|
members = listOf(member(otherAci), member(selfAci, joinedAt = 2))
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(2) {
|
||||||
|
fullSnapshot(serverState, title = "Baking Signal for Science")
|
||||||
|
}
|
||||||
|
changeLog(3) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Breaking Signal for Science")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
doReturn(true).`when`(groupDatabase).isUnknownGroup(any())
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(2, 0, DecryptedGroupChange.getDefaultInstance())
|
||||||
|
|
||||||
|
assertThat("local should update to revision added", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches peer revision added", result.latestServer!!.revision, `is`(2))
|
||||||
|
assertThat("title matches that as it was in revision added", result.latestServer!!.title, `is`("Baking Signal for Science"))
|
||||||
|
|
||||||
|
verify(ApplicationDependencies.getJobManager()).add(isA(RequestGroupV2InfoJob::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when learning of a group via storage service, then update from server to latest revision`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 10,
|
||||||
|
title = "Stargate Fan Club",
|
||||||
|
description = "Indeed.",
|
||||||
|
members = selfAndOthers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
|
||||||
|
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches latest server", result.latestServer!!.revision, `is`(10))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when request to join group is approved, with no group changes after approved, then update from server to revision we were added`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = GroupsV2StateProcessor.PLACEHOLDER_REVISION,
|
||||||
|
title = "Beam me up",
|
||||||
|
requestingMembers = listOf(requestingMember(selfAci))
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 3,
|
||||||
|
title = "Beam me up",
|
||||||
|
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(3) {
|
||||||
|
fullSnapshot(serverState)
|
||||||
|
change {
|
||||||
|
addNewMembers(member(selfAci, joinedAt = 3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
||||||
|
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches server", result.latestServer!!.revision, `is`(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when request to join group is approved, with group changes occurring after approved, then update from server to revision we were added, and then schedule pulling additional changes later`() {
|
||||||
|
given {
|
||||||
|
localState(
|
||||||
|
revision = GroupsV2StateProcessor.PLACEHOLDER_REVISION,
|
||||||
|
title = "Beam me up",
|
||||||
|
requestingMembers = listOf(requestingMember(selfAci))
|
||||||
|
)
|
||||||
|
serverState(
|
||||||
|
revision = 5,
|
||||||
|
title = "Beam me up!",
|
||||||
|
members = listOf(member(otherAci), member(selfAci, joinedAt = 3))
|
||||||
|
)
|
||||||
|
changeSet {
|
||||||
|
changeLog(3) {
|
||||||
|
fullSnapshot(extendGroup = serverState, title = "Beam me up")
|
||||||
|
change {
|
||||||
|
addNewMembers(member(selfAci, joinedAt = 3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeLog(4) {
|
||||||
|
change {
|
||||||
|
setNewTitle("May the force be with you")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeLog(5) {
|
||||||
|
change {
|
||||||
|
setNewTitle("Beam me up!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiCallParameters(requestedRevision = 3, includeFirst = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = processor.updateLocalGroupToRevision(3, 0, null)
|
||||||
|
|
||||||
|
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
|
||||||
|
assertThat("revision matches revision approved at", result.latestServer!!.revision, `is`(3))
|
||||||
|
assertThat("title matches revision approved at", result.latestServer!!.title, `is`("Beam me up"))
|
||||||
|
verify(ApplicationDependencies.getJobManager()).add(isA(RequestGroupV2InfoJob::class.java))
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ public final class DecryptedGroupHistoryEntry {
|
||||||
private final Optional<DecryptedGroup> group;
|
private final Optional<DecryptedGroup> group;
|
||||||
private final Optional<DecryptedGroupChange> change;
|
private final Optional<DecryptedGroupChange> change;
|
||||||
|
|
||||||
DecryptedGroupHistoryEntry(Optional<DecryptedGroup> group, Optional<DecryptedGroupChange> change)
|
public DecryptedGroupHistoryEntry(Optional<DecryptedGroup> group, Optional<DecryptedGroupChange> change)
|
||||||
throws InvalidGroupStateException
|
throws InvalidGroupStateException
|
||||||
{
|
{
|
||||||
if (group.isPresent() && change.isPresent() && group.get().getRevision() != change.get().getRevision()) {
|
if (group.isPresent() && change.isPresent() && group.get().getRevision() != change.get().getRevision()) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.signal.zkgroup.auth.AuthCredentialResponse;
|
||||||
import org.signal.zkgroup.auth.ClientZkAuthOperations;
|
import org.signal.zkgroup.auth.ClientZkAuthOperations;
|
||||||
import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
import org.signal.zkgroup.groups.ClientZkGroupCipher;
|
||||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||||
|
import org.whispersystems.libsignal.logging.Log;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.push.ACI;
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
@ -31,9 +32,8 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public final class GroupsV2Api {
|
public class GroupsV2Api {
|
||||||
|
|
||||||
private final PushServiceSocket socket;
|
private final PushServiceSocket socket;
|
||||||
private final GroupsV2Operations groupsOperations;
|
private final GroupsV2Operations groupsOperations;
|
||||||
|
@ -97,20 +97,15 @@ public final class GroupsV2Api {
|
||||||
|
|
||||||
public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
|
public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
|
||||||
int fromRevision,
|
int fromRevision,
|
||||||
GroupsV2AuthorizationString authorization)
|
GroupsV2AuthorizationString authorization,
|
||||||
|
boolean includeFirstState)
|
||||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||||
{
|
{
|
||||||
List<GroupChanges.GroupChangeState> changesList = new LinkedList<>();
|
PushServiceSocket.GroupHistory group = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState);
|
||||||
PushServiceSocket.GroupHistory group;
|
List<DecryptedGroupHistoryEntry> result = new ArrayList<>(group.getGroupChanges().getGroupChangesList().size());
|
||||||
|
|
||||||
group = socket.getGroupsV2GroupHistory(fromRevision, authorization);
|
|
||||||
|
|
||||||
changesList.addAll(group.getGroupChanges().getGroupChangesList());
|
|
||||||
|
|
||||||
ArrayList<DecryptedGroupHistoryEntry> result = new ArrayList<>(changesList.size());
|
|
||||||
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
|
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
|
||||||
|
|
||||||
for (GroupChanges.GroupChangeState change : changesList) {
|
for (GroupChanges.GroupChangeState change : group.getGroupChanges().getGroupChangesList()) {
|
||||||
Optional<DecryptedGroup> decryptedGroup = change.hasGroupState() ? Optional.of(groupOperations.decryptGroup(change.getGroupState())) : Optional.absent();
|
Optional<DecryptedGroup> decryptedGroup = change.hasGroupState() ? Optional.of(groupOperations.decryptGroup(change.getGroupState())) : Optional.absent();
|
||||||
Optional<DecryptedGroupChange> decryptedChange = change.hasGroupChange() ? groupOperations.decryptChange(change.getGroupChange(), false) : Optional.absent();
|
Optional<DecryptedGroupChange> decryptedChange = change.hasGroupChange() ? groupOperations.decryptChange(change.getGroupChange(), false) : Optional.absent();
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,7 @@ public class PushServiceSocket {
|
||||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
||||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||||
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
|
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
|
||||||
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s";
|
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
|
||||||
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
|
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
|
||||||
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
|
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
|
||||||
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
|
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
|
||||||
|
@ -2381,11 +2381,11 @@ public class PushServiceSocket {
|
||||||
return GroupChange.parseFrom(readBodyBytes(response));
|
return GroupChange.parseFrom(readBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupHistory getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization)
|
public GroupHistory getGroupsV2GroupHistory(int fromVersion, GroupsV2AuthorizationString authorization, int highestKnownEpoch, boolean includeFirstState)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
Response response = makeStorageRequestResponse(authorization.toString(),
|
Response response = makeStorageRequestResponse(authorization.toString(),
|
||||||
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion),
|
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion, highestKnownEpoch, includeFirstState),
|
||||||
"GET",
|
"GET",
|
||||||
null,
|
null,
|
||||||
GROUPS_V2_GET_LOGS_HANDLER);
|
GROUPS_V2_GET_LOGS_HANDLER);
|
||||||
|
|
Ładowanie…
Reference in New Issue