Refactor group state processing.

This commit is contained in:
Cody Henthorne
2024-05-22 10:00:17 -04:00
parent 1296365bed
commit 6362da7a50
39 changed files with 1930 additions and 2009 deletions

View File

@@ -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
}
}

View File

@@ -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")
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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,

View File

@@ -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
{

View File

@@ -12,6 +12,10 @@ public final class InvalidGroupStateException extends Exception {
super(e);
}
InvalidGroupStateException(String message) {
super(message);
}
InvalidGroupStateException() {
}
}

View File

@@ -2,7 +2,7 @@ package org.whispersystems.signalservice.api.groupsv2;
public final class NotAbleToApplyGroupV2ChangeException extends Exception {
NotAbleToApplyGroupV2ChangeException() {
public NotAbleToApplyGroupV2ChangeException() {
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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
{