Write previous group state to the database for advanced change messages.

master
Alan Evans 2020-10-06 11:21:56 -03:00 committed by GitHub
parent b40fd7b243
commit 38fa58c0a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 40 deletions

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -81,7 +82,11 @@ final class GroupsV2UpdateMessageProducer {
} }
} }
List<UpdateDescription> describeChanges(@NonNull DecryptedGroupChange change) { List<UpdateDescription> describeChanges(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) {
if (DecryptedGroup.getDefaultInstance().equals(previousGroupState)) {
previousGroupState = null;
}
List<UpdateDescription> updates = new LinkedList<>(); List<UpdateDescription> updates = new LinkedList<>();
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) { if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
@ -96,7 +101,7 @@ final class GroupsV2UpdateMessageProducer {
describeUnknownEditorNewTimer(change, updates); describeUnknownEditorNewTimer(change, updates);
describeUnknownEditorNewAttributeAccess(change, updates); describeUnknownEditorNewAttributeAccess(change, updates);
describeUnknownEditorNewMembershipAccess(change, updates); describeUnknownEditorNewMembershipAccess(change, updates);
describeUnknownEditorNewGroupInviteLinkAccess(change, updates); describeUnknownEditorNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates); describeRequestingMembers(change, updates);
describeUnknownEditorRequestingMembersApprovals(change, updates); describeUnknownEditorRequestingMembersApprovals(change, updates);
describeUnknownEditorRequestingMembersDeletes(change, updates); describeUnknownEditorRequestingMembersDeletes(change, updates);
@ -119,7 +124,7 @@ final class GroupsV2UpdateMessageProducer {
describeNewTimer(change, updates); describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates); describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates); describeNewMembershipAccess(change, updates);
describeNewGroupInviteLinkAccess(change, updates); describeNewGroupInviteLinkAccess(previousGroupState, change, updates);
describeRequestingMembers(change, updates); describeRequestingMembers(change, updates);
describeRequestingMembersApprovals(change, updates); describeRequestingMembersApprovals(change, updates);
describeRequestingMembersDeletes(change, updates); describeRequestingMembersDeletes(change, updates);
@ -509,7 +514,16 @@ final class GroupsV2UpdateMessageProducer {
} }
} }
private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) { private void describeNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change,
@NonNull List<UpdateDescription> updates)
{
AccessControl.AccessRequired previousAccessControl = null;
if (previousGroupState != null) {
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
}
boolean editorIsYou = change.getEditor().equals(selfUuidBytes); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
boolean groupLinkEnabled = false; boolean groupLinkEnabled = false;
@ -517,17 +531,33 @@ final class GroupsV2UpdateMessageProducer {
case ANY: case ANY:
groupLinkEnabled = true; groupLinkEnabled = true;
if (editorIsYou) { if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off))); if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_admin_approval_for_the_group_link)));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off)));
}
} else { } else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor))); if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, editor)));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor)));
}
} }
break; break;
case ADMINISTRATOR: case ADMINISTRATOR:
groupLinkEnabled = true; groupLinkEnabled = true;
if (editorIsYou) { if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on))); if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_admin_approval_for_the_group_link)));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on)));
}
} else { } else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor))); if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, editor)));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor)));
}
} }
break; break;
case UNSATISFIABLE: case UNSATISFIABLE:
@ -548,13 +578,30 @@ final class GroupsV2UpdateMessageProducer {
} }
} }
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) { private void describeUnknownEditorNewGroupInviteLinkAccess(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change,
@NonNull List<UpdateDescription> updates)
{
AccessControl.AccessRequired previousAccessControl = null;
if (previousGroupState != null) {
previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink();
}
switch (change.getNewInviteLinkAccess()) { switch (change.getNewInviteLinkAccess()) {
case ANY: case ANY:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off))); if (previousAccessControl == AccessControl.AccessRequired.ADMINISTRATOR) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off)));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off)));
}
break; break;
case ADMINISTRATOR: case ADMINISTRATOR:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on))); if (previousAccessControl == AccessControl.AccessRequired.ANY) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on)));
} else {
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on)));
}
break; break;
case UNSATISFIABLE: case UNSATISFIABLE:
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off))); updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off)));

View File

@ -177,7 +177,7 @@ public abstract class MessageRecord extends DisplayRecord {
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get()); GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) { if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange())); return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
} else { } else {
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange()); return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
} }

View File

@ -193,7 +193,7 @@ final class GroupManagerV2 {
.setEditor(UuidUtil.toByteString(selfUuid)) .setEditor(UuidUtil.toByteString(selfUuid))
.build(); .build();
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, groupChange, null); RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
recipientAndThread.threadId, recipientAndThread.threadId,
@ -505,16 +505,18 @@ final class GroupManagerV2 {
private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change) private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{ {
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties(); final GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
final int nextRevision = v2GroupProperties.getGroupRevision() + 1; final int nextRevision = v2GroupProperties.getGroupRevision() + 1;
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build(); final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
final DecryptedGroupChange decryptedChange; final DecryptedGroupChange decryptedChange;
final DecryptedGroup decryptedGroupState; final DecryptedGroup decryptedGroupState;
final DecryptedGroup previousGroupState;
try { try {
previousGroupState = v2GroupProperties.getDecryptedGroup();
decryptedChange = groupOperations.decryptChange(changeActions, selfUuid); decryptedChange = groupOperations.decryptChange(changeActions, selfUuid);
decryptedGroupState = DecryptedGroupUtil.apply(v2GroupProperties.getDecryptedGroup(), decryptedChange); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
Log.w(TAG, e); Log.w(TAG, e);
throw new IOException(e); throw new IOException(e);
@ -523,7 +525,8 @@ final class GroupManagerV2 {
GroupChange signedGroupChange = commitToServer(changeActions); GroupChange signedGroupChange = commitToServer(changeActions);
groupDatabase.update(groupId, decryptedGroupState); groupDatabase.update(groupId, decryptedGroupState);
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange); GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState);
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange);
int newMembersCount = decryptedChange.getNewMembersCount(); int newMembersCount = decryptedChange.getNewMembersCount();
List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList()); List<RecipientId> newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList());
@ -681,7 +684,7 @@ final class GroupManagerV2 {
} else if (requestToJoin) { } else if (requestToJoin) {
Log.i(TAG, "Requested to join, cannot send update"); Log.i(TAG, "Requested to join, cannot send update");
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
return new GroupManager.GroupActionResult(groupRecipient, return new GroupManager.GroupActionResult(groupRecipient,
recipientAndThread.threadId, recipientAndThread.threadId,
@ -706,7 +709,7 @@ final class GroupManagerV2 {
System.currentTimeMillis(), System.currentTimeMillis(),
decryptedChange); decryptedChange);
RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); RecipientAndThread recipientAndThread = sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
return new GroupManager.GroupActionResult(groupRecipient, return new GroupManager.GroupActionResult(groupRecipient,
recipientAndThread.threadId, recipientAndThread.threadId,
@ -905,7 +908,7 @@ final class GroupManagerV2 {
groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.getRevision())); groupDatabase.update(groupId, resetRevision(newGroup, decryptedGroup.getRevision()));
sendGroupUpdate(groupMasterKey, decryptedGroup, decryptedChange, signedGroupChange); sendGroupUpdate(groupMasterKey, new GroupMutation(decryptedGroup, decryptedChange, newGroup), signedGroupChange);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} }
@ -959,13 +962,12 @@ final class GroupManagerV2 {
} }
private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey, private @NonNull RecipientAndThread sendGroupUpdate(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup, @NonNull GroupMutation groupMutation,
@Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedGroupChange) @Nullable GroupChange signedGroupChange)
{ {
GroupId.V2 groupId = GroupId.v2(masterKey); GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient groupRecipient = Recipient.externalGroup(context, groupId); Recipient groupRecipient = Recipient.externalGroup(context, groupId);
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange, signedGroupChange); DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, groupMutation, signedGroupChange);
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
decryptedGroupV2Context, decryptedGroupV2Context,
null, null,
@ -977,8 +979,11 @@ final class GroupManagerV2 {
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();
if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) { if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) {
ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, decryptedGroup, outgoingMessage)); ApplicationDependencies.getJobManager().add(PushGroupSilentUpdateSendJob.create(context, groupId, groupMutation.getNewGroupState(), outgoingMessage));
return new RecipientAndThread(groupRecipient, -1); return new RecipientAndThread(groupRecipient, -1);
} else { } else {
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.groups;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
public final class GroupMutation {
@Nullable private final DecryptedGroup previousGroupState;
@Nullable private final DecryptedGroupChange groupChange;
@NonNull private final DecryptedGroup newGroupState;
public GroupMutation(@Nullable DecryptedGroup previousGroupState, @Nullable DecryptedGroupChange groupChange, @NonNull DecryptedGroup newGroupState) {
this.previousGroupState = previousGroupState;
this.groupChange = groupChange;
this.newGroupState = newGroupState;
}
public @Nullable DecryptedGroup getPreviousGroupState() {
return previousGroupState;
}
public @Nullable DecryptedGroupChange getGroupChange() {
return groupChange;
}
public @NonNull DecryptedGroup getNewGroupState() {
return newGroupState;
}
}

View File

@ -49,12 +49,13 @@ public final class GroupProtoUtil {
} }
public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey, public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup, @NonNull GroupMutation groupMutation,
@Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedServerChange) @Nullable GroupChange signedServerChange)
{ {
int revision = plainGroupChange != null ? plainGroupChange.getRevision() : decryptedGroup.getRevision(); DecryptedGroupChange plainGroupChange = groupMutation.getGroupChange();
SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder() DecryptedGroup decryptedGroup = groupMutation.getNewGroupState();
int revision = plainGroupChange != null ? plainGroupChange.getRevision() : decryptedGroup.getRevision();
SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(masterKey.serialize())) .setMasterKey(ByteString.copyFrom(masterKey.serialize()))
.setRevision(revision); .setRevision(revision);
@ -66,6 +67,10 @@ public final class GroupProtoUtil {
.setContext(contextBuilder.build()) .setContext(contextBuilder.build())
.setGroupState(decryptedGroup); .setGroupState(decryptedGroup);
if (groupMutation.getPreviousGroupState() != null) {
builder.setPreviousGroupState(groupMutation.getPreviousGroupState());
}
if (plainGroupChange != null) { if (plainGroupChange != null) {
builder.setChange(plainGroupChange); builder.setChange(plainGroupChange);
} }

View File

@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMutation;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException; import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.GroupProtoUtil; import org.thoughtcrime.securesms.groups.GroupProtoUtil;
import org.thoughtcrime.securesms.groups.GroupsV2Authorization; import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
@ -226,9 +227,9 @@ public final class GroupsV2StateProcessor {
determineProfileSharing(inputGroupState, newLocalState); determineProfileSharing(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, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
} else { } else {
insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries()); insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
} }
persistLearnedProfileKeys(inputGroupState); persistLearnedProfileKeys(inputGroupState);
@ -260,7 +261,7 @@ public final class GroupsV2StateProcessor {
.addDeleteMembers(UuidUtil.toByteString(selfUuid)) .addDeleteMembers(UuidUtil.toByteString(selfUuid))
.build(); .build();
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange, 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,
@ -362,13 +363,17 @@ public final class GroupsV2StateProcessor {
} }
} }
private void insertUpdateMessages(long timestamp, Collection<LocalGroupLogEntry> processedLogEntries) { private void insertUpdateMessages(long timestamp,
@Nullable DecryptedGroup previousGroupState,
Collection<LocalGroupLogEntry> processedLogEntries)
{
for (LocalGroupLogEntry entry : processedLogEntries) { for (LocalGroupLogEntry entry : processedLogEntries) {
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) { if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) {
Log.d(TAG, "Skipping profile key changes only update message"); Log.d(TAG, "Skipping profile key changes only update message");
} else { } else {
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp); storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(previousGroupState, entry.getChange(), entry.getGroup()), null), timestamp);
} }
previousGroupState = entry.getGroup();
} }
} }

View File

@ -28,9 +28,10 @@ import "SignalService.proto";
import "DecryptedGroups.proto"; import "DecryptedGroups.proto";
message DecryptedGroupV2Context { message DecryptedGroupV2Context {
signalservice.GroupContextV2 context = 1; signalservice.GroupContextV2 context = 1;
DecryptedGroupChange change = 2; DecryptedGroupChange change = 2;
DecryptedGroup groupState = 3; DecryptedGroup groupState = 3;
DecryptedGroup previousGroupState = 4;
} }
message TemporalAuthCredentialResponse { message TemporalAuthCredentialResponse {

View File

@ -1013,6 +1013,12 @@
<string name="MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off">The group link has been turned on with admin approval off.</string> <string name="MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off">The group link has been turned on with admin approval off.</string>
<string name="MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on">The group link has been turned on with admin approval on.</string> <string name="MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on">The group link has been turned on with admin approval on.</string>
<string name="MessageRecord_the_group_link_has_been_turned_off">The group link has been turned off.</string> <string name="MessageRecord_the_group_link_has_been_turned_off">The group link has been turned off.</string>
<string name="MessageRecord_you_turned_off_admin_approval_for_the_group_link">You turned off admin approval for the group link.</string>
<string name="MessageRecord_s_turned_off_admin_approval_for_the_group_link">%1$s turned off admin approval for the group link.</string>
<string name="MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off">The admin approval for the group link has been turned off.</string>
<string name="MessageRecord_you_turned_on_admin_approval_for_the_group_link">You turned on admin approval for the group link.</string>
<string name="MessageRecord_s_turned_on_admin_approval_for_the_group_link">%1$s turned on admin approval for the group link.</string>
<string name="MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on">The admin approval for the group link has been turned on.</string>
<!-- GV2 group link reset --> <!-- GV2 group link reset -->
<string name="MessageRecord_you_reset_the_group_link">You reset the group link.</string> <string name="MessageRecord_you_reset_the_group_link">You reset the group link.</string>

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
import android.app.Application; import android.app.Application;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -35,6 +36,7 @@ import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -921,6 +923,56 @@ public final class GroupsV2UpdateMessageProducerTest {
assertThat(describeChange(change), is(singletonList("The group link has been turned off."))); assertThat(describeChange(change), is(singletonList("The group link has been turned off.")));
} }
// Group link with known previous group state
@Test
public void group_link_access_from_unknown_to_administrator() {
assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNKNOWN, AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void group_link_access_from_administrator_to_unsatisfiable() {
assertEquals("You turned off the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE));
assertEquals("Bob turned off the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE));
assertEquals("The group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.UNSATISFIABLE));
}
@Test
public void group_link_access_from_unsatisfiable_to_administrator() {
assertEquals("You turned on the group link with admin approval on.", describeGroupLinkChange(you, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("Alice turned on the group link with admin approval on.", describeGroupLinkChange(alice, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("The group link has been turned on with admin approval on.", describeGroupLinkChange(null, AccessControl.AccessRequired.UNSATISFIABLE, AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void group_link_access_from_administrator_to_any() {
assertEquals("You turned off admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY));
assertEquals("Bob turned off admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY));
assertEquals("The admin approval for the group link has been turned off.", describeGroupLinkChange(null, AccessControl.AccessRequired.ADMINISTRATOR, AccessControl.AccessRequired.ANY));
}
@Test
public void group_link_access_from_any_to_administrator() {
assertEquals("You turned on admin approval for the group link.", describeGroupLinkChange(you, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("Bob turned on admin approval for the group link.", describeGroupLinkChange(bob, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR));
assertEquals("The admin approval for the group link has been turned on.", describeGroupLinkChange(null, AccessControl.AccessRequired.ANY, AccessControl.AccessRequired.ADMINISTRATOR));
}
private String describeGroupLinkChange(@Nullable UUID editor, @NonNull AccessControl.AccessRequired fromAccess, AccessControl.AccessRequired toAccess){
DecryptedGroup previousGroupState = DecryptedGroup.newBuilder()
.setAccessControl(AccessControl.newBuilder()
.setAddFromInviteLink(fromAccess))
.build();
DecryptedGroupChange change = (editor != null ? changeBy(editor) : changeByUnknown()).inviteLinkAccess(toAccess)
.build();
List<String> strings = describeChange(previousGroupState, change);
assertEquals(1, strings.size());
return strings.get(0);
}
// Group link reset // Group link reset
@Test @Test
@ -1271,8 +1323,14 @@ public final class GroupsV2UpdateMessageProducerTest {
} }
private @NonNull List<String> describeChange(@NonNull DecryptedGroupChange change) { private @NonNull List<String> describeChange(@NonNull DecryptedGroupChange change) {
return describeChange(null, change);
}
private @NonNull List<String> describeChange(@Nullable DecryptedGroup previousGroupState,
@NonNull DecryptedGroupChange change)
{
MainThreadUtil.setMainThread(false); MainThreadUtil.setMainThread(false);
return Stream.of(producer.describeChanges(change)) return Stream.of(producer.describeChanges(previousGroupState, change))
.map(UpdateDescription::getString) .map(UpdateDescription::getString)
.toList(); .toList();
} }
@ -1291,7 +1349,7 @@ public final class GroupsV2UpdateMessageProducerTest {
} }
private void assertSingleChangeMentioning(DecryptedGroupChange change, List<UUID> expectedMentions) { private void assertSingleChangeMentioning(DecryptedGroupChange change, List<UUID> expectedMentions) {
List<UpdateDescription> changes = producer.describeChanges(change); List<UpdateDescription> changes = producer.describeChanges(null, change);
assertThat(changes.size(), is(1)); assertThat(changes.size(), is(1));