Add Group Send Endorsements support.

This commit is contained in:
Cody Henthorne
2024-07-08 12:47:20 -04:00
parent 414368e251
commit f5abd7acdf
86 changed files with 1691 additions and 887 deletions

View File

@@ -172,6 +172,16 @@ public final class GroupManager {
}
}
@WorkerThread
public static void updateGroupSendEndorsements(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.updateGroupSendEndorsements();
}
}
@WorkerThread
public static void setMemberAdmin(@NonNull Context context,
@NonNull GroupId.V2 groupId,

View File

@@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChangeResponse;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -49,6 +50,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
@@ -219,16 +222,18 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
DecryptedGroup decryptedGroup;
DecryptedGroupResponse createGroupResponse;
try {
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer);
createGroupResponse = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer);
} catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e);
}
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
DecryptedGroup decryptedGroup = createGroupResponse.getGroup();
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
ReceivedGroupSendEndorsements groupSendEndorsements = groupsV2Operations.forGroup(groupSecretParams).receiveGroupSendEndorsements(selfAci, decryptedGroup, createGroupResponse.getGroupSendEndorsementsResponse());
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup, groupSendEndorsements);
if (groupId == null) {
throw new GroupChangeFailedException("Unable to create group, group already exists");
@@ -670,7 +675,8 @@ final class GroupManagerV2 {
previousGroupState = v2GroupProperties.getDecryptedGroup();
GroupChange signedGroupChange = commitToServer(changeActions);
GroupChangeResponse changeResponse = commitToServer(changeActions);
GroupChange signedGroupChange = changeResponse.groupChange;
try {
//noinspection OptionalGetWithoutIsPresent
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
@@ -680,7 +686,7 @@ final class GroupManagerV2 {
throw new IOException(e);
}
groupDatabase.update(groupId, decryptedGroupState);
groupDatabase.update(groupId, decryptedGroupState, groupsV2Operations.forGroup(groupSecretParams).receiveGroupSendEndorsements(selfAci, decryptedGroupState, changeResponse.groupSendEndorsementsResponse));
GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers);
@@ -690,7 +696,7 @@ final class GroupManagerV2 {
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
}
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
private @NonNull GroupChangeResponse commitToServer(@NonNull GroupChange.Actions change)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{
try {
@@ -747,6 +753,14 @@ final class GroupManagerV2 {
.forceSanityUpdateFromServer(timestamp);
}
@WorkerThread
void updateGroupSendEndorsements()
throws IOException, GroupNotAMemberException
{
GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.updateGroupSendEndorsements();
}
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
if (signedGroupChange != null && signedGroupChange.length > 0) {
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
@@ -764,11 +778,11 @@ final class GroupManagerV2 {
}
@WorkerThread
private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members,
int disappearingMessageTimerSeconds)
private @NonNull DecryptedGroupResponse createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members,
int disappearingMessageTimerSeconds)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(members)) {
@@ -797,15 +811,10 @@ final class GroupManagerV2 {
disappearingMessageTimerSeconds);
try {
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
DecryptedGroupResponse groupResponse = groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, AppDependencies.getGroupsV2Authorization().getAuthorizationForToday(serviceIds, groupSecretParams));
if (decryptedGroup == null) {
throw new GroupChangeFailedException();
}
return decryptedGroup;
} catch (VerificationFailedException | InvalidGroupStateException e) {
return groupResponse;
} catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
throw new GroupChangeFailedException(e);
} catch (GroupExistsException e) {
throw new GroupAlreadyExistsException(e);
@@ -837,8 +846,9 @@ final class GroupManagerV2 {
@Nullable byte[] avatar)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException
{
boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
boolean alreadyAMember = false;
boolean requestToJoin = joinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
boolean alreadyAMember = false;
boolean groupAlreadyExists = false;
if (requestToJoin) {
Log.i(TAG, "Requesting to join " + groupId);
@@ -846,10 +856,13 @@ final class GroupManagerV2 {
Log.i(TAG, "Joining " + groupId);
}
GroupChange signedGroupChange = null;
DecryptedGroupChange decryptedChange = null;
GroupChangeResponse groupChangeResponse = null;
GroupChange signedGroupChange = null;
DecryptedGroupChange decryptedChange = null;
try {
signedGroupChange = joinGroupOnServer(requestToJoin, joinInfo.revision);
groupChangeResponse = joinGroupOnServer(requestToJoin, joinInfo.revision);
signedGroupChange = groupChangeResponse.groupChange;
if (requestToJoin) {
Log.i(TAG, String.format("Successfully requested to join %s on server", groupId));
@@ -857,7 +870,7 @@ final class GroupManagerV2 {
Log.i(TAG, String.format("Successfully added self to %s on server", groupId));
}
decryptedChange = decryptChange(signedGroupChange);
decryptedChange = decryptChange(Objects.requireNonNull(signedGroupChange));
} catch (GroupJoinAlreadyAMemberException e) {
Log.i(TAG, "Server reports that we are already a member of " + groupId);
alreadyAMember = true;
@@ -869,28 +882,37 @@ final class GroupManagerV2 {
if (group.isPresent()) {
Log.i(TAG, "Group already present locally");
if (decryptedChange != null) {
try {
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to apply join change to existing group", e);
}
}
groupAlreadyExists = true;
} else {
GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup);
GroupId.V2 groupId = groupDatabase.create(groupMasterKey, decryptedGroup, null);
if (groupId != null) {
Log.i(TAG, "Created local group with placeholder");
} else {
Log.i(TAG, "Create placeholder failed, group suddenly present locally, attempting to apply change");
if (decryptedChange != null) {
try {
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to apply join change to existing group", e);
}
}
Log.i(TAG, "Create placeholder failed, group suddenly present locally");
groupAlreadyExists = true;
}
}
if (groupAlreadyExists) {
Log.i(TAG, "Attempting to update local group with change/server");
try {
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange != null ? decryptedChange.revision : GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Despite adding self to group, change/server says we are not a member, scheduling refresh of group info " + groupId, e);
AppDependencies.getJobManager()
.add(new RequestGroupV2InfoJob(groupId));
throw new GroupChangeFailedException(e);
} catch (IOException e) {
Log.w(TAG, "Group data fetch failed, scheduling refresh of group info " + groupId, e);
AppDependencies.getJobManager()
.add(new RequestGroupV2InfoJob(groupId));
throw e;
}
}
@@ -1006,7 +1028,7 @@ final class GroupManagerV2 {
return group.build();
}
private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision)
private @NonNull GroupChangeResponse joinGroupOnServer(boolean requestToJoin, int currentRevision)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
{
if (!GroupsV2CapabilityChecker.allAndSelfHaveServiceId(Collections.singleton(Recipient.self().getId()))) {
@@ -1029,7 +1051,7 @@ final class GroupManagerV2 {
return commitJoinChangeWithConflictResolution(currentRevision, change);
}
private @NonNull GroupChange commitJoinChangeWithConflictResolution(int currentRevision, @NonNull GroupChange.Actions.Builder change)
private @NonNull GroupChangeResponse commitJoinChangeWithConflictResolution(int currentRevision, @NonNull GroupChange.Actions.Builder change)
throws GroupChangeFailedException, IOException, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
{
for (int attempt = 0; attempt < 5; attempt++) {
@@ -1038,10 +1060,10 @@ final class GroupManagerV2 {
.build();
Log.i(TAG, "Trying to join group at V" + changeActions.revision);
GroupChange signedGroupChange = commitJoinToServer(changeActions);
GroupChangeResponse changeResponse = commitJoinToServer(changeActions);
Log.i(TAG, "Successfully joined group at V" + changeActions.revision);
return signedGroupChange;
return changeResponse;
} catch (GroupPatchNotAcceptedException e) {
Log.w(TAG, "Patch not accepted", e);
@@ -1051,7 +1073,7 @@ final class GroupManagerV2 {
} else {
throw new GroupChangeFailedException(e);
}
} catch (VerificationFailedException | InvalidGroupStateException ex) {
} catch (VerificationFailedException | InvalidGroupStateException | InvalidInputException ex) {
throw new GroupChangeFailedException(ex);
}
} catch (ConflictException e) {
@@ -1064,7 +1086,7 @@ final class GroupManagerV2 {
throw new GroupChangeFailedException("Unable to join group after conflicts");
}
private @NonNull GroupChange commitJoinToServer(@NonNull GroupChange.Actions change)
private @NonNull GroupChangeResponse commitJoinToServer(@NonNull GroupChange.Actions change)
throws GroupChangeFailedException, IOException, GroupLinkNotActiveException
{
try {
@@ -1109,7 +1131,7 @@ final class GroupManagerV2 {
}
private boolean testGroupMembership()
throws IOException, VerificationFailedException, InvalidGroupStateException
throws IOException, VerificationFailedException, InvalidGroupStateException, InvalidInputException
{
try {
groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
@@ -1140,7 +1162,7 @@ final class GroupManagerV2 {
DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange);
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.revision));
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.revision), null);
sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange, false);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
@@ -1165,7 +1187,7 @@ final class GroupManagerV2 {
.build();
Log.i(TAG, "Trying to cancel request group at V" + changeActions.revision);
GroupChange signedGroupChange = commitJoinToServer(changeActions);
GroupChange signedGroupChange = commitJoinToServer(changeActions).groupChange;
Log.i(TAG, "Successfully cancelled group join at V" + changeActions.revision);
return signedGroupChange;

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.groups.v2.processing
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
@@ -9,10 +10,15 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog
*/
class GroupStateDiff(
val previousGroupState: DecryptedGroup?,
val serverHistory: List<DecryptedGroupChangeLog>
val serverHistory: List<DecryptedGroupChangeLog>,
val groupSendEndorsementsResponse: GroupSendEndorsementsResponse?
) {
constructor(previousGroupState: DecryptedGroup?, changedGroupState: DecryptedGroup?, change: DecryptedGroupChange?) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change)))
constructor(
previousGroupState: DecryptedGroup?,
changedGroupState: DecryptedGroup?,
change: DecryptedGroupChange?
) : this(previousGroupState, listOf(DecryptedGroupChangeLog(changedGroupState, change)), null)
val earliestRevisionNumber: Int
get() {

View File

@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.groups.GroupSecretParams
@@ -43,6 +44,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceIds
@@ -95,8 +97,9 @@ class GroupsV2StateProcessor private constructor(
}
}
private val groupsApi = AppDependencies.signalServiceAccountManager.groupsV2Api
private val groupsApi = AppDependencies.signalServiceAccountManager.getGroupsV2Api()
private val groupsV2Authorization = AppDependencies.groupsV2Authorization
private val groupOperations = AppDependencies.groupsV2Operations.forGroup(groupSecretParams)
private val groupId = GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier())
private val profileAndMessageHelper = ProfileAndMessageHelper.create(serviceIds.aci, groupMasterKey, groupId)
@@ -124,6 +127,36 @@ class GroupsV2StateProcessor private constructor(
}
}
/**
* Fetch and save the latest group send endorsements from the server. This endorsements returned may
* not match our local view of the membership if the membership has changed on the server and we haven't updated the
* group state yet. This is only an issue when trying to send to a group member that has been removed and should be handled
* gracefully as a fallback in the sending flow.
*/
@WorkerThread
@Throws(IOException::class, GroupNotAMemberException::class)
fun updateGroupSendEndorsements() {
val result = groupsApi.getGroupAsResult(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
val groupResponse = when (result) {
is NetworkResult.Success -> result.result
else -> when (val cause = result.getCause()!!) {
is NotInGroupException, is GroupNotFoundException -> throw GroupNotAMemberException(cause)
is IOException -> throw cause
else -> throw IOException(cause)
}
}
val receivedGroupSendEndorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, groupResponse.group, groupResponse.groupSendEndorsementsResponse)
if (receivedGroupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Updating group send endorsements")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, receivedGroupSendEndorsements)
} else {
Log.w(TAG, "$logPrefix No group send endorsements on response")
}
}
/**
* Using network where required, will attempt to bring the local copy of the group up to the revision specified.
*
@@ -233,7 +266,8 @@ class GroupsV2StateProcessor private constructor(
return saveGroupUpdate(
timestamp = timestamp,
serverGuid = serverGuid,
groupStateDiff = groupStateDiff
groupStateDiff = groupStateDiff,
groupSendEndorsements = null
)
}
@@ -259,6 +293,8 @@ class GroupsV2StateProcessor private constructor(
else -> return InternalUpdateResult.from(result.getCause()!!)
}
val sendEndorsementExpiration = groupRecord.map { it.groupSendEndorsementExpiration }.orElse(0L)
var includeFirstState = currentLocalState == null ||
currentLocalState.revision < 0 ||
currentLocalState.revision == joinedAtRevision ||
@@ -273,20 +309,30 @@ class GroupsV2StateProcessor private constructor(
var hasRemainingRemoteChanges = false
while (hasMore) {
Log.i(TAG, "$logPrefix Requesting change logs from server, currentRevision=${currentLocalState?.revision ?: "null"} logsNeededFrom=$logsNeededFrom includeFirstState=$includeFirstState")
Log.i(TAG, "$logPrefix Requesting change logs from server, currentRevision=${currentLocalState?.revision ?: "null"} logsNeededFrom=$logsNeededFrom includeFirstState=$includeFirstState sendEndorsementExpiration=${sendEndorsementExpiration > 0}")
val (remoteGroupStateDiff, pagingData) = getGroupChangeLogs(currentLocalState, logsNeededFrom, includeFirstState)
val (remoteGroupStateDiff, pagingData) = getGroupChangeLogs(currentLocalState, logsNeededFrom, includeFirstState, sendEndorsementExpiration)
val applyGroupStateDiffResult: AdvanceGroupStateResult = GroupStatePatcher.applyGroupStateDiff(remoteGroupStateDiff, targetRevision)
val updatedGroupState: DecryptedGroup? = applyGroupStateDiffResult.updatedGroupState
if (updatedGroupState == null || updatedGroupState == remoteGroupStateDiff.previousGroupState) {
Log.i(TAG, "$logPrefix Local state is at or later than server revision: ${currentLocalState?.revision ?: "null"}")
if (currentLocalState != null) {
val endorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, currentLocalState, remoteGroupStateDiff.groupSendEndorsementsResponse)
if (endorsements != null) {
Log.i(TAG, "$logPrefix Received updated send endorsements, saving")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, endorsements)
}
}
return InternalUpdateResult.NoUpdateNeeded
}
Log.i(TAG, "$logPrefix Saving updated group state at revision: ${updatedGroupState.revision}")
saveGroupState(remoteGroupStateDiff, updatedGroupState)
saveGroupState(remoteGroupStateDiff, updatedGroupState, groupOperations.receiveGroupSendEndorsements(serviceIds.aci, updatedGroupState, remoteGroupStateDiff.groupSendEndorsementsResponse))
if (addMessagesForAllUpdates) {
Log.d(TAG, "$logPrefix Inserting group changes into chat history")
@@ -320,7 +366,7 @@ class GroupsV2StateProcessor private constructor(
}
if (!addMessagesForAllUpdates) {
Log.i(TAG, "Inserting single update message for restore placeholder")
Log.i(TAG, "$logPrefix Inserting single update message for restore placeholder")
profileAndMessageHelper.insertUpdateMessages(runningTimestamp, null, setOf(AppliedGroupChangeLog(currentLocalState!!, null)), serverGuid)
}
@@ -341,19 +387,20 @@ class GroupsV2StateProcessor private constructor(
private fun updateToLatestViaServer(timestamp: Long, currentLocalState: DecryptedGroup?, reconstructChange: Boolean): InternalUpdateResult {
val result = groupsApi.getGroupAsResult(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
val serverState = if (result is NetworkResult.Success) {
val groupResponse = if (result is NetworkResult.Success) {
result.result
} else {
return InternalUpdateResult.from(result.getCause()!!)
}
val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, serverState) else null
val remoteGroupStateDiff = GroupStateDiff(currentLocalState, serverState, completeGroupChange)
val completeGroupChange = if (reconstructChange) GroupChangeReconstruct.reconstructGroupChange(currentLocalState, groupResponse.group) else null
val remoteGroupStateDiff = GroupStateDiff(currentLocalState, groupResponse.group, completeGroupChange)
return saveGroupUpdate(
timestamp = timestamp,
serverGuid = null,
groupStateDiff = remoteGroupStateDiff
groupStateDiff = remoteGroupStateDiff,
groupSendEndorsements = groupOperations.receiveGroupSendEndorsements(serviceIds.aci, groupResponse.group, groupResponse.groupSendEndorsementsResponse)
)
}
@@ -403,22 +450,30 @@ class GroupsV2StateProcessor private constructor(
}
@Throws(IOException::class)
private fun getGroupChangeLogs(localState: DecryptedGroup?, logsNeededFromRevision: Int, includeFirstState: Boolean): Pair<GroupStateDiff, GroupHistoryPage.PagingData> {
private fun getGroupChangeLogs(
localState: DecryptedGroup?,
logsNeededFromRevision: Int,
includeFirstState: Boolean,
sendEndorsementsExpirationMs: Long
): Pair<GroupStateDiff, GroupHistoryPage.PagingData> {
try {
val groupHistoryPage = groupsApi.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState)
val groupHistoryPage = groupsApi.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState, sendEndorsementsExpirationMs)
return GroupStateDiff(localState, groupHistoryPage.changeLogs) to groupHistoryPage.pagingData
return GroupStateDiff(localState, groupHistoryPage.changeLogs, groupHistoryPage.groupSendEndorsementsResponse) to groupHistoryPage.pagingData
} catch (e: InvalidGroupStateException) {
throw IOException(e)
} catch (e: VerificationFailedException) {
throw IOException(e)
} catch (e: InvalidInputException) {
throw IOException(e)
}
}
private fun saveGroupUpdate(
timestamp: Long,
serverGuid: String?,
groupStateDiff: GroupStateDiff
groupStateDiff: GroupStateDiff,
groupSendEndorsements: ReceivedGroupSendEndorsements?
): InternalUpdateResult {
val currentLocalState: DecryptedGroup? = groupStateDiff.previousGroupState
val applyGroupStateDiffResult = GroupStatePatcher.applyGroupStateDiff(groupStateDiff, GroupStatePatcher.LATEST)
@@ -426,12 +481,18 @@ class GroupsV2StateProcessor private constructor(
if (updatedGroupState == null || updatedGroupState == groupStateDiff.previousGroupState) {
Log.i(TAG, "$logPrefix Local state and server state are equal")
if (groupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Saving new send endorsements")
SignalDatabase.groups.updateGroupSendEndorsements(groupId, groupSendEndorsements)
}
return InternalUpdateResult.NoUpdateNeeded
} else {
Log.i(TAG, "$logPrefix Local state (revision: ${currentLocalState?.revision}) does not match, updating to ${updatedGroupState.revision}")
}
saveGroupState(groupStateDiff, updatedGroupState)
saveGroupState(groupStateDiff, updatedGroupState, groupSendEndorsements)
if (currentLocalState == null || currentLocalState.revision == RESTORE_PLACEHOLDER_REVISION) {
Log.i(TAG, "$logPrefix Inserting single update message for no local state or restore placeholder")
@@ -453,20 +514,24 @@ class GroupsV2StateProcessor private constructor(
return InternalUpdateResult.Updated(updatedGroupState)
}
private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup) {
private fun saveGroupState(groupStateDiff: GroupStateDiff, updatedGroupState: DecryptedGroup, groupSendEndorsements: ReceivedGroupSendEndorsements?) {
val previousGroupState = groupStateDiff.previousGroupState
if (groupSendEndorsements != null) {
Log.i(TAG, "$logPrefix Updating send endorsements")
}
val needsAvatarFetch = if (previousGroupState == null) {
val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState)
val groupId = SignalDatabase.groups.create(groupMasterKey, updatedGroupState, groupSendEndorsements)
if (groupId == null) {
Log.w(TAG, "$logPrefix Group create failed, trying to update")
SignalDatabase.groups.update(groupMasterKey, updatedGroupState)
SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements)
}
updatedGroupState.avatar.isNotEmpty()
} else {
SignalDatabase.groups.update(groupMasterKey, updatedGroupState)
SignalDatabase.groups.update(groupMasterKey, updatedGroupState, groupSendEndorsements)
updatedGroupState.avatar != previousGroupState.avatar
}