GV2 Update message description.

This commit is contained in:
Alan Evans 2020-05-14 13:59:34 -03:00 committed by GitHub
parent b917cccbee
commit 4c5822ac67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 200 additions and 23 deletions

View file

@ -8,6 +8,7 @@ import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member; 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.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
@ -16,6 +17,8 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.LinkedList; import java.util.LinkedList;
@ -26,18 +29,50 @@ final class GroupsV2UpdateMessageProducer {
@NonNull private final Context context; @NonNull private final Context context;
@NonNull private final DescribeMemberStrategy descriptionStrategy; @NonNull private final DescribeMemberStrategy descriptionStrategy;
@NonNull private final ByteString youUuid; @NonNull private final UUID selfUuid;
@NonNull private final ByteString selfUuidBytes;
/** /**
* @param descriptionStrategy Strategy for member description. * @param descriptionStrategy Strategy for member description.
*/ */
GroupsV2UpdateMessageProducer(@NonNull Context context, GroupsV2UpdateMessageProducer(@NonNull Context context,
@NonNull DescribeMemberStrategy descriptionStrategy, @NonNull DescribeMemberStrategy descriptionStrategy,
@NonNull UUID you) @NonNull UUID selfUuid)
{ {
this.context = context; this.context = context;
this.descriptionStrategy = descriptionStrategy; this.descriptionStrategy = descriptionStrategy;
this.youUuid = UuidUtil.toByteString(you); this.selfUuid = selfUuid;
this.selfUuidBytes = UuidUtil.toByteString(selfUuid);
}
/**
* Describes a group that is new to you, use this when there is no available change record.
* <p>
* Invitation and groups you create are the most common cases where no change is available.
*/
String describeNewGroup(@NonNull DecryptedGroup group) {
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
if (selfPending.isPresent()) {
return context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(selfPending.get().getAddedByUuid()));
}
if (group.getVersion() == 0) {
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(group.getMembersList());
if (foundingMember.isPresent()) {
ByteString foundingMemberUuid = foundingMember.get().getUuid();
if (selfUuidBytes.equals(foundingMemberUuid)) {
return context.getString(R.string.MessageRecord_you_created_the_group);
} else {
return context.getString(R.string.MessageRecord_s_added_you, describe(foundingMemberUuid));
}
}
}
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) {
return context.getString(R.string.MessageRecord_you_joined_the_group);
} else {
return context.getString(R.string.MessageRecord_group_updated);
}
} }
List<String> describeChange(@NonNull DecryptedGroupChange change) { List<String> describeChange(@NonNull DecryptedGroupChange change) {
@ -66,7 +101,7 @@ final class GroupsV2UpdateMessageProducer {
* Handles case of future protocol versions where we don't know what has changed. * Handles case of future protocol versions where we don't know what has changed.
*/ */
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_updated_group)); updates.add(context.getString(R.string.MessageRecord_you_updated_group));
@ -76,10 +111,10 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (DecryptedMember member : change.getNewMembersList()) { for (DecryptedMember member : change.getNewMembersList()) {
boolean newMemberIsYou = member.getUuid().equals(youUuid); boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
if (newMemberIsYou) { if (newMemberIsYou) {
@ -102,10 +137,10 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (ByteString member : change.getDeleteMembersList()) { for (ByteString member : change.getDeleteMembersList()) {
boolean newMemberIsYou = member.equals(youUuid); boolean newMemberIsYou = member.equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
if (newMemberIsYou) { if (newMemberIsYou) {
@ -128,11 +163,11 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) { for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
boolean newMemberIsYou = roleChange.getUuid().equals(youUuid); boolean newMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid()))); updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid())));
} else { } else {
@ -144,7 +179,7 @@ final class GroupsV2UpdateMessageProducer {
} }
} }
} else { } else {
boolean newMemberIsYou = roleChange.getUuid().equals(youUuid); boolean newMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid()))); updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid())));
} else { } else {
@ -159,11 +194,11 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
int notYouInviteCount = 0; int notYouInviteCount = 0;
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
boolean newMemberIsYou = invitee.getUuid().equals(youUuid); boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
if (newMemberIsYou) { if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(change.getEditor()))); updates.add(context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(change.getEditor())));
@ -182,7 +217,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
int notDeclineCount = 0; int notDeclineCount = 0;
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
@ -208,11 +243,11 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (DecryptedMember newMember : change.getPromotePendingMembersList()) { for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
ByteString uuid = newMember.getUuid(); ByteString uuid = newMember.getUuid();
boolean newMemberIsYou = uuid.equals(youUuid); boolean newMemberIsYou = uuid.equals(selfUuidBytes);
if (editorIsYou) { if (editorIsYou) {
if (newMemberIsYou) { if (newMemberIsYou) {
@ -235,7 +270,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.hasNewTitle()) { if (change.hasNewTitle()) {
if (editorIsYou) { if (editorIsYou) {
@ -247,7 +282,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.hasNewAvatar()) { if (change.hasNewAvatar()) {
if (editorIsYou) { if (editorIsYou) {
@ -259,7 +294,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.hasNewTimer()) { if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration()); String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
@ -272,7 +307,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) { if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess()); String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
@ -285,7 +320,7 @@ final class GroupsV2UpdateMessageProducer {
} }
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) { private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid); boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) { if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess()); String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());

View file

@ -17,22 +17,31 @@
package org.thoughtcrime.securesms.database.model; package org.thoughtcrime.securesms.database.model;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.UUID;
/** /**
* The base class for message record models that are displayed in * The base class for message record models that are displayed in
@ -44,6 +53,8 @@ import java.util.List;
*/ */
public abstract class MessageRecord extends DisplayRecord { public abstract class MessageRecord extends DisplayRecord {
private static final String TAG = Log.tag(MessageRecord.class);
private final Recipient individualRecipient; private final Recipient individualRecipient;
private final int recipientDeviceId; private final int recipientDeviceId;
private final long id; private final long id;
@ -96,7 +107,9 @@ public abstract class MessageRecord extends DisplayRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
if (isGroupUpdate() && isOutgoing()) { if (isGroupUpdate() && isGroupV2()) {
return new SpannableString(getGv2Description(context));
} else if (isGroupUpdate() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
} else if (isGroupUpdate()) { } else if (isGroupUpdate()) {
return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient())); return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient()));
@ -134,6 +147,56 @@ public abstract class MessageRecord extends DisplayRecord {
return new SpannableString(getBody()); return new SpannableString(getBody());
} }
private @NonNull String getGv2Description(@NonNull Context context) {
if (!isGroupUpdate() || !isGroupV2()) {
throw new AssertionError();
}
try {
ShortStringDescriptionStrategy descriptionStrategy = new ShortStringDescriptionStrategy(context);
byte[] decoded = Base64.decode(getBody());
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getVersion() > 0) {
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
List<String> strings = updateMessageProducer.describeChange(change);
StringBuilder result = new StringBuilder();
for (int i = 0; i < strings.size(); i++) {
if (i > 0) result.append('\n');
result.append(strings.get(i));
}
return result.toString();
} else {
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState());
}
} catch (IOException e) {
Log.w(TAG, "GV2 Message update detail could not be read", e);
return context.getString(R.string.MessageRecord_group_updated);
}
}
/**
* Describes a UUID by it's corresponding recipient's {@link Recipient#toShortString}.
*/
private static class ShortStringDescriptionStrategy implements GroupsV2UpdateMessageProducer.DescribeMemberStrategy {
private final Context context;
ShortStringDescriptionStrategy(@NonNull Context context) {
this.context = context;
}
@Override
public @NonNull String describe(@NonNull UUID uuid) {
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
return context.getString(R.string.MessageRecord_unknown);
}
return Recipient.resolved(RecipientId.from(uuid, null)).toShortString(context);
}
}
public long getId() { public long getId() {
return id; return id;
} }

View file

@ -14,6 +14,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member; 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.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole; import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
@ -491,6 +492,84 @@ public final class GroupsV2UpdateMessageProducerTest {
"Alice changed who can edit group membership to \"All members\"."))); "Alice changed who can edit group membership to \"All members\".")));
} }
// Group state without a change record
@Test
public void you_created_a_group() {
DecryptedGroup group = newGroupBy(you, 0)
.build();
assertThat(producer.describeNewGroup(group), is("You created the group."));
}
@Test
public void alice_created_a_group() {
DecryptedGroup group = newGroupBy(alice, 0)
.member(you)
.build();
assertThat(producer.describeNewGroup(group), is("Alice added you to the group."));
}
@Test
public void alice_created_a_group_above_zero() {
DecryptedGroup group = newGroupBy(alice, 1)
.member(you)
.build();
assertThat(producer.describeNewGroup(group), is("You joined the group."));
}
@Test
public void you_were_invited_to_a_group() {
DecryptedGroup group = newGroupBy(alice, 0)
.invite(bob, you)
.build();
assertThat(producer.describeNewGroup(group), is("Bob invited you to the group."));
}
@Test
public void describe_a_group_you_are_not_in() {
DecryptedGroup group = newGroupBy(alice, 1)
.build();
assertThat(producer.describeNewGroup(group), is("Group updated."));
}
private GroupStateBuilder newGroupBy(UUID foundingMember, int revision) {
return new GroupStateBuilder(foundingMember, revision);
}
private static class GroupStateBuilder {
private final DecryptedGroup.Builder builder;
GroupStateBuilder(@NonNull UUID foundingMember, int version) {
builder = DecryptedGroup.newBuilder()
.setVersion(version)
.addMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(foundingMember)));
}
GroupStateBuilder invite(@NonNull UUID inviter, @NonNull UUID invitee) {
builder.addPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(invitee))
.setAddedByUuid(UuidUtil.toByteString(inviter)));
return this;
}
GroupStateBuilder member(@NonNull UUID member) {
builder.addMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(member)));
return this;
}
public DecryptedGroup build() {
return builder.build();
}
}
private static class ChangeBuilder { private static class ChangeBuilder {
private final DecryptedGroupChange.Builder builder; private final DecryptedGroupChange.Builder builder;