mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-01 06:03:18 +01:00
Refactor group state processing.
This commit is contained in:
@@ -30,13 +30,14 @@ sealed class NetworkResult<T>(
|
||||
companion object {
|
||||
/**
|
||||
* A convenience method to capture the common case of making a request.
|
||||
* Perform the network action in the [fetch] lambda, returning your result.
|
||||
* Perform the network action in the [fetcher], returning your result.
|
||||
* Common exceptions will be caught and translated to errors.
|
||||
*/
|
||||
fun <T> fromFetch(fetch: () -> T): NetworkResult<T> = try {
|
||||
Success(fetch())
|
||||
@JvmStatic
|
||||
fun <T> fromFetch(fetcher: Fetcher<T>): NetworkResult<T> = try {
|
||||
Success(fetcher.fetch())
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
StatusCodeError(e.code, e.body, e)
|
||||
StatusCodeError(e)
|
||||
} catch (e: IOException) {
|
||||
NetworkError(e)
|
||||
} catch (e: Throwable) {
|
||||
@@ -51,7 +52,9 @@ sealed class NetworkResult<T>(
|
||||
data class NetworkError<T>(val exception: IOException) : NetworkResult<T>()
|
||||
|
||||
/** Indicates we got a response, but it was a non-2xx response. */
|
||||
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>()
|
||||
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>() {
|
||||
constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.body, e)
|
||||
}
|
||||
|
||||
/** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
|
||||
data class ApplicationError<T>(val throwable: Throwable) : NetworkResult<T>()
|
||||
@@ -175,4 +178,9 @@ sealed class NetworkResult<T>(
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun interface Fetcher<T> {
|
||||
@Throws(Exception::class)
|
||||
fun fetch(): T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||
|
||||
/**
|
||||
* A changelog from the server representing a specific group state revision. The
|
||||
* log can contain:
|
||||
*
|
||||
* 1. A full group snapshot for the revision
|
||||
* 2. A full group snapshot and the change from the previous revision to achieve the snapshot
|
||||
* 3. Only the change from the previous revision to achieve this revision
|
||||
*
|
||||
* Most often, it will be the change only (3).
|
||||
*/
|
||||
data class DecryptedGroupChangeLog(val group: DecryptedGroup?, val change: DecryptedGroupChange?) {
|
||||
|
||||
val revision: Int
|
||||
get() = group?.revision ?: change!!.revision
|
||||
|
||||
init {
|
||||
if (group == null && change == null) {
|
||||
throw InvalidGroupStateException("group and change are both null")
|
||||
}
|
||||
|
||||
if (group != null && change != null && group.revision != change.revision) {
|
||||
throw InvalidGroupStateException("group revision != change revision")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* Pair of a {@link DecryptedGroup} and the {@link DecryptedGroupChange} for that version.
|
||||
*/
|
||||
public final class DecryptedGroupHistoryEntry {
|
||||
|
||||
private final Optional<DecryptedGroup> group;
|
||||
private final Optional<DecryptedGroupChange> change;
|
||||
|
||||
public DecryptedGroupHistoryEntry(Optional<DecryptedGroup> group, Optional<DecryptedGroupChange> change)
|
||||
throws InvalidGroupStateException
|
||||
{
|
||||
if (group.isPresent() && change.isPresent() && group.get().revision != change.get().revision) {
|
||||
throw new InvalidGroupStateException();
|
||||
}
|
||||
|
||||
this.group = group;
|
||||
this.change = change;
|
||||
}
|
||||
|
||||
public Optional<DecryptedGroup> getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public Optional<DecryptedGroupChange> getChange() {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wraps result of group history fetch with it's associated paging data.
|
||||
*/
|
||||
public final class GroupHistoryPage {
|
||||
|
||||
private final List<DecryptedGroupHistoryEntry> results;
|
||||
private final PagingData pagingData;
|
||||
|
||||
|
||||
public GroupHistoryPage(List<DecryptedGroupHistoryEntry> results, PagingData pagingData) {
|
||||
this.results = results;
|
||||
this.pagingData = pagingData;
|
||||
}
|
||||
|
||||
public List<DecryptedGroupHistoryEntry> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public PagingData getPagingData() {
|
||||
return pagingData;
|
||||
}
|
||||
|
||||
public static final class PagingData {
|
||||
public static final PagingData NONE = new PagingData(false, -1);
|
||||
|
||||
private final boolean hasMorePages;
|
||||
private final int nextPageRevision;
|
||||
|
||||
public static PagingData fromGroup(PushServiceSocket.GroupHistory groupHistory) {
|
||||
return new PagingData(groupHistory.hasMore(), groupHistory.hasMore() ? groupHistory.getNextPageStartGroupRevision() : -1);
|
||||
}
|
||||
|
||||
private PagingData(boolean hasMorePages, int nextPageRevision) {
|
||||
this.hasMorePages = hasMorePages;
|
||||
this.nextPageRevision = nextPageRevision;
|
||||
}
|
||||
|
||||
public boolean hasMorePages() {
|
||||
return hasMorePages;
|
||||
}
|
||||
|
||||
public int getNextPageRevision() {
|
||||
return nextPageRevision;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket.GroupHistory
|
||||
|
||||
/**
|
||||
* Wraps result of group history fetch with it's associated paging data.
|
||||
*/
|
||||
data class GroupHistoryPage(val changeLogs: List<DecryptedGroupChangeLog>, val pagingData: PagingData) {
|
||||
|
||||
data class PagingData(val hasMorePages: Boolean, val nextPageRevision: Int) {
|
||||
companion object {
|
||||
@JvmField
|
||||
val NONE = PagingData(false, -1)
|
||||
|
||||
@JvmStatic
|
||||
fun forGroupHistory(groupHistory: GroupHistory): PagingData {
|
||||
return PagingData(groupHistory.hasMore(), if (groupHistory.hasMore()) groupHistory.nextPageStartGroupRevision else -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.whispersystems.signalservice.api.NetworkResult;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
@@ -32,6 +33,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class GroupsV2Api {
|
||||
@@ -87,14 +90,8 @@ public class GroupsV2Api {
|
||||
socket.putNewGroupsV2Group(group, authorization);
|
||||
}
|
||||
|
||||
public PartialDecryptedGroup getPartialDecryptedGroup(GroupSecretParams groupSecretParams,
|
||||
GroupsV2AuthorizationString authorization)
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||
{
|
||||
Group group = socket.getGroupsV2Group(authorization);
|
||||
|
||||
return groupsOperations.forGroup(groupSecretParams)
|
||||
.partialDecryptGroup(group);
|
||||
public NetworkResult<DecryptedGroup> getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
|
||||
return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization));
|
||||
}
|
||||
|
||||
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
|
||||
@@ -114,17 +111,21 @@ public class GroupsV2Api {
|
||||
throws IOException, InvalidGroupStateException, VerificationFailedException
|
||||
{
|
||||
PushServiceSocket.GroupHistory group = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState);
|
||||
List<DecryptedGroupHistoryEntry> result = new ArrayList<>(group.getGroupChanges().groupChanges.size());
|
||||
List<DecryptedGroupChangeLog> result = new ArrayList<>(group.getGroupChanges().groupChanges.size());
|
||||
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
|
||||
|
||||
for (GroupChanges.GroupChangeState change : group.getGroupChanges().groupChanges) {
|
||||
Optional<DecryptedGroup> decryptedGroup = change.groupState != null ? Optional.of(groupOperations.decryptGroup(change.groupState)) : Optional.empty();
|
||||
Optional<DecryptedGroupChange> decryptedChange = change.groupChange != null ? groupOperations.decryptChange(change.groupChange, false) : Optional.empty();
|
||||
DecryptedGroup decryptedGroup = change.groupState != null ? groupOperations.decryptGroup(change.groupState) : null;
|
||||
DecryptedGroupChange decryptedChange = change.groupChange != null ? groupOperations.decryptChange(change.groupChange, false).orElse(null) : null;
|
||||
|
||||
result.add(new DecryptedGroupHistoryEntry(decryptedGroup, decryptedChange));
|
||||
result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange));
|
||||
}
|
||||
|
||||
return new GroupHistoryPage(result, GroupHistoryPage.PagingData.fromGroup(group));
|
||||
return new GroupHistoryPage(result, GroupHistoryPage.PagingData.forGroupHistory(group));
|
||||
}
|
||||
|
||||
public NetworkResult<Integer> getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) {
|
||||
return NetworkResult.fromFetch(() -> socket.getGroupJoinedAtRevision(authorization));
|
||||
}
|
||||
|
||||
public DecryptedGroupJoinInfo getGroupJoinInfo(GroupSecretParams groupSecretParams,
|
||||
|
||||
@@ -425,35 +425,6 @@ public final class GroupsV2Operations {
|
||||
return new PendingMember.Builder().member(member);
|
||||
}
|
||||
|
||||
public PartialDecryptedGroup partialDecryptGroup(Group group)
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
List<Member> membersList = group.members;
|
||||
List<PendingMember> pendingMembersList = group.pendingMembers;
|
||||
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
|
||||
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
|
||||
|
||||
for (Member member : membersList) {
|
||||
ACI memberAci = decryptAci(member.userId);
|
||||
decryptedMembers.add(new DecryptedMember.Builder().aciBytes(memberAci.toByteString())
|
||||
.joinedAtRevision(member.joinedAtRevision)
|
||||
.build());
|
||||
}
|
||||
|
||||
for (PendingMember member : pendingMembersList) {
|
||||
ServiceId pendingMemberServiceId = decryptServiceIdOrUnknown(member.member.userId);
|
||||
decryptedPendingMembers.add(new DecryptedPendingMember.Builder().serviceIdBytes(pendingMemberServiceId.toByteString()).build());
|
||||
}
|
||||
|
||||
DecryptedGroup decryptedGroup = new DecryptedGroup.Builder()
|
||||
.revision(group.revision)
|
||||
.members(decryptedMembers)
|
||||
.pendingMembers(decryptedPendingMembers)
|
||||
.build();
|
||||
|
||||
return new PartialDecryptedGroup(group, decryptedGroup, GroupsV2Operations.this, groupSecretParams);
|
||||
}
|
||||
|
||||
public DecryptedGroup decryptGroup(Group group)
|
||||
throws VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
|
||||
@@ -12,6 +12,10 @@ public final class InvalidGroupStateException extends Exception {
|
||||
super(e);
|
||||
}
|
||||
|
||||
InvalidGroupStateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
InvalidGroupStateException() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
public final class NotAbleToApplyGroupV2ChangeException extends Exception {
|
||||
|
||||
NotAbleToApplyGroupV2ChangeException() {
|
||||
public NotAbleToApplyGroupV2ChangeException() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.storageservice.protos.groups.Group;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Decrypting an entire group can be expensive for large groups. Since not every
|
||||
* operation requires all data to be decrypted, this class can be populated with only
|
||||
* the minimalist about of information need to perform an operation. Currently, only
|
||||
* updating from the server utilizes it.
|
||||
*/
|
||||
public class PartialDecryptedGroup {
|
||||
private final Group group;
|
||||
private final DecryptedGroup decryptedGroup;
|
||||
private final GroupsV2Operations groupsOperations;
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
|
||||
public PartialDecryptedGroup(Group group,
|
||||
DecryptedGroup decryptedGroup,
|
||||
GroupsV2Operations groupsOperations,
|
||||
GroupSecretParams groupSecretParams)
|
||||
{
|
||||
this.group = group;
|
||||
this.decryptedGroup = decryptedGroup;
|
||||
this.groupsOperations = groupsOperations;
|
||||
this.groupSecretParams = groupSecretParams;
|
||||
}
|
||||
|
||||
public int getRevision() {
|
||||
return decryptedGroup.revision;
|
||||
}
|
||||
|
||||
public List<DecryptedMember> getMembersList() {
|
||||
return decryptedGroup.members;
|
||||
}
|
||||
|
||||
public List<DecryptedPendingMember> getPendingMembersList() {
|
||||
return decryptedGroup.pendingMembers;
|
||||
}
|
||||
|
||||
public DecryptedGroup getFullyDecryptedGroup()
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
return groupsOperations.forGroup(groupSecretParams)
|
||||
.decryptGroup(group);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ sealed class ServiceId(val libSignalServiceId: LibSignalServiceId) {
|
||||
|
||||
/** Parses a ServiceId serialized as a ByteString. Returns null if the ServiceId is invalid. */
|
||||
@JvmStatic
|
||||
fun parseOrNull(bytes: okio.ByteString): ServiceId? = parseOrNull(bytes.toByteArray())
|
||||
fun parseOrNull(bytes: okio.ByteString?): ServiceId? = parseOrNull(bytes?.toByteArray())
|
||||
|
||||
/** Parses a ServiceId serialized as a string. Crashes if the ServiceId is invalid. */
|
||||
@JvmStatic
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChanges;
|
||||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest;
|
||||
@@ -274,6 +275,7 @@ public class PushServiceSocket {
|
||||
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_TOKEN = "/v1/groups/token";
|
||||
private static final String GROUPSV2_JOINED_AT = "/v1/groups/joined_at_version";
|
||||
|
||||
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
|
||||
|
||||
@@ -2844,6 +2846,19 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
public int getGroupJoinedAtRevision(GroupsV2AuthorizationString authorization)
|
||||
throws IOException
|
||||
{
|
||||
try (Response response = makeStorageRequest(authorization.toString(),
|
||||
GROUPSV2_JOINED_AT,
|
||||
"GET",
|
||||
null,
|
||||
NO_HANDLER))
|
||||
{
|
||||
return Member.ADAPTER.decode(readBodyBytes(response)).joinedAtRevision;
|
||||
}
|
||||
}
|
||||
|
||||
public GroupJoinInfo getGroupJoinInfo(Optional<byte[]> groupLinkPassword, GroupsV2AuthorizationString authorization)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user