Add support for GV2 group update messages.

This commit is contained in:
Alan Evans 2020-04-06 15:51:32 -03:00 committed by Greyson Parrelli
parent 1f994495f8
commit 326678f214
4 changed files with 996 additions and 0 deletions

View File

@ -0,0 +1,310 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
final class GroupsV2UpdateMessageProducer {
@NonNull private final Context context;
@NonNull private final DescribeMemberStrategy descriptionStrategy;
@NonNull private final ByteString youUuid;
/**
* @param descriptionStrategy Strategy for member description.
*/
GroupsV2UpdateMessageProducer(@NonNull Context context,
@NonNull DescribeMemberStrategy descriptionStrategy,
@NonNull UUID you)
{
this.context = context;
this.descriptionStrategy = descriptionStrategy;
this.youUuid = UuidUtil.toByteString(you);
}
List<String> describeChange(@NonNull DecryptedGroupChange change) {
List<String> updates = new LinkedList<>();
describeMemberAdditions(change, updates);
describeMemberRemovals(change, updates);
describeModifyMemberRoles(change, updates);
describeInvitations(change, updates);
describeRevokedInvitations(change, updates);
describePromotePending(change, updates);
describeNewTitle(change, updates);
describeNewAvatar(change, updates);
describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates);
if (updates.isEmpty()) {
describeUnknownChange(change, updates);
}
return updates;
}
/**
* Handles case of future protocol versions where we don't know what has changed.
*/
private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_updated_group));
} else {
updates.add(context.getString(R.string.MessageRecord_s_updated_group, describe(change.getEditor())));
}
}
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
for (DecryptedMember member : change.getNewMembersList()) {
boolean newMemberIsYou = member.getUuid().equals(youUuid);
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_you_added_s, describe(member.getUuid())));
}
} else {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
} else {
if (member.getUuid().equals(change.getEditor())) {
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
} else {
updates.add(context.getString(R.string.MessageRecord_s_added_s, describe(change.getEditor()), describe(member.getUuid())));
}
}
}
}
}
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
for (ByteString member : change.getDeleteMembersList()) {
boolean newMemberIsYou = member.equals(youUuid);
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_left_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_you_removed_s, describe(member)));
}
} else {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_removed_you_from_the_group, describe(change.getEditor())));
} else {
if (member.equals(change.getEditor())) {
updates.add(context.getString(R.string.MessageRecord_s_left_the_group, describe(member)));
} else {
updates.add(context.getString(R.string.MessageRecord_s_removed_s, describe(change.getEditor()), describe(member)));
}
}
}
}
}
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
boolean newMemberIsYou = roleChange.getUuid().equals(youUuid);
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid())));
} else {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_made_you_an_admin, describe(change.getEditor())));
} else {
updates.add(context.getString(R.string.MessageRecord_s_made_s_an_admin, describe(change.getEditor()), describe(roleChange.getUuid())));
}
}
} else {
boolean newMemberIsYou = roleChange.getUuid().equals(youUuid);
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid())));
} else {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, describe(change.getEditor())));
} else {
updates.add(context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, describe(change.getEditor()), describe(roleChange.getUuid())));
}
}
}
}
}
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
int notYouInviteCount = 0;
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
boolean newMemberIsYou = invitee.getUuid().equals(youUuid);
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_invited_you_to_the_group, describe(change.getEditor())));
} else {
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_invited_s_to_the_group, describe(invitee.getUuid())));
} else {
notYouInviteCount++;
}
}
}
if (notYouInviteCount > 0) {
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_invited_members, notYouInviteCount, describe(change.getEditor()), notYouInviteCount));
}
}
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
int notDeclineCount = 0;
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
boolean decline = invitee.getUuid().equals(change.getEditor());
if (decline) {
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group));
}
} else {
notDeclineCount++;
}
}
if (notDeclineCount > 0) {
if (editorIsYou) {
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount));
} else {
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_s_revoked_invites, notDeclineCount, describe(change.getEditor()), notDeclineCount));
}
}
}
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
for (ByteString member : change.getPromotePendingMembersList()) {
boolean newMemberIsYou = member.equals(youUuid);
if (editorIsYou) {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_accepted_invite));
} else {
updates.add(context.getString(R.string.MessageRecord_you_added_invited_member_s, describe(member)));
}
} else {
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_added_you, describe(change.getEditor())));
} else {
if (member.equals(change.getEditor())) {
updates.add(context.getString(R.string.MessageRecord_s_accepted_invite, describe(member)));
} else {
updates.add(context.getString(R.string.MessageRecord_s_added_invited_member_s, describe(change.getEditor()), describe(member)));
}
}
}
}
}
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (change.hasNewTitle()) {
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, change.getNewTitle().getValue()));
} else {
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_name_to_s, describe(change.getEditor()), change.getNewTitle().getValue()));
}
}
}
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (change.hasNewAvatar()) {
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_changed_the_group_avatar));
} else {
updates.add(context.getString(R.string.MessageRecord_s_changed_the_group_avatar, describe(change.getEditor())));
}
}
}
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time));
} else {
updates.add(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, describe(change.getEditor()), time));
}
}
}
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel));
} else {
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, describe(change.getEditor()), accessLevel));
}
}
}
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(youUuid);
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel));
} else {
updates.add(context.getString(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, describe(change.getEditor()), accessLevel));
}
}
}
private @NonNull String describe(@NonNull ByteString uuid) {
return descriptionStrategy.describe(UuidUtil.fromByteString(uuid));
}
interface DescribeMemberStrategy {
/**
* Map a UUID to a string that describes the group member.
*/
@NonNull String describe(@NonNull UUID uuid);
}
}

View File

@ -0,0 +1,23 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.storageservice.protos.groups.AccessControl;
import org.thoughtcrime.securesms.R;
public final class GV2AccessLevelUtil {
private GV2AccessLevelUtil() {
}
public static String toString(@NonNull Context context, @NonNull AccessControl.AccessRequired attributeAccess) {
switch (attributeAccess) {
case ANY : return context.getString(R.string.GroupManagement_access_level_anyone);
case MEMBER : return context.getString(R.string.GroupManagement_access_level_all_members);
case ADMINISTRATOR : return context.getString(R.string.GroupManagement_access_level_only_admins);
default : return context.getString(R.string.GroupManagement_access_level_unknown);
}
}
}

View File

@ -442,6 +442,12 @@
<!-- GroupMembersDialog -->
<string name="GroupMembersDialog_you">You</string>
<!-- GV2 access levels -->
<string name="GroupManagement_access_level_anyone">Anyone</string>
<string name="GroupManagement_access_level_all_members">All members</string>
<string name="GroupManagement_access_level_only_admins">Only admins</string>
<string name="GroupManagement_access_level_unknown" translatable="false">Unknown</string>
<!-- PendingMembersActivity -->
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group invites</string>
<string name="PendingMembersActivity_people_you_invited">People you invited</string>
@ -614,6 +620,74 @@
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s.</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s.</string>
<!-- GV2 specific -->
<!-- GV2 member additions -->
<string name="MessageRecord_you_added_s">You added %1$s.</string>
<string name="MessageRecord_s_added_s">%1$s added %2$s.</string>
<string name="MessageRecord_s_added_you">%1$s added you to the group.</string>
<string name="MessageRecord_you_joined_the_group">You joined the group.</string>
<string name="MessageRecord_s_joined_the_group">%1$s joined the group.</string>
<!-- GV2 member removals -->
<string name="MessageRecord_you_removed_s">You removed %1$s.</string>
<string name="MessageRecord_s_removed_s">%1$s removed %2$s.</string>
<string name="MessageRecord_s_removed_you_from_the_group">%1$s removed you from the group.</string>
<string name="MessageRecord_you_left_the_group">You left the group.</string>
<string name="MessageRecord_s_left_the_group">%1$s left the group.</string>
<!-- GV2 role change -->
<string name="MessageRecord_you_made_s_an_admin">You made %1$s an admin.</string>
<string name="MessageRecord_s_made_s_an_admin">%1$s made %2$s an admin.</string>
<string name="MessageRecord_s_made_you_an_admin">%1$s made you an admin.</string>
<string name="MessageRecord_you_revoked_admin_privileges_from_s">You revoked admin privileges from %1$s.</string>
<string name="MessageRecord_s_revoked_your_admin_privileges">%1$s revoked your admin privileges."</string>
<string name="MessageRecord_s_revoked_admin_privileges_from_s">%1$s revoked admin privileges from %2$s.</string>
<!-- GV2 invitations -->
<string name="MessageRecord_you_invited_s_to_the_group">You invited %1$s to the group.</string>
<string name="MessageRecord_s_invited_you_to_the_group">%1$s invited you to the group.</string>
<plurals name="MessageRecord_s_invited_members">
<item quantity="one">%1$s invited 1 person to the group.</item>
<item quantity="other">%1$s invited %2$d people to the group.</item>
</plurals>
<!-- GV2 invitation revokes -->
<plurals name="MessageRecord_you_revoked_invites">
<item quantity="one">You revoked an invitation to the group.</item>
<item quantity="other">You revoked %1$d invitations to the group.</item>
</plurals>
<plurals name="MessageRecord_s_revoked_invites">
<item quantity="one">%1$s revoked an invitation to the group.</item>
<item quantity="other">%1$s revoked %2$d invitations to the group.</item>
</plurals>
<string name="MessageRecord_someone_declined_an_invitation_to_the_group">Someone declined an invitation to the group.</string>
<string name="MessageRecord_you_declined_the_invitation_to_the_group">You declined the invitation to the group.</string>
<!-- GV2 invitation acceptance -->
<string name="MessageRecord_you_accepted_invite">You accepted the invitation to the group.</string>
<string name="MessageRecord_s_accepted_invite">%1$s accepted an invitation to the group.</string>
<string name="MessageRecord_you_added_invited_member_s">You added invited member %1$s.</string>
<string name="MessageRecord_s_added_invited_member_s">%1$s added invited member %2$s.</string>
<!-- GV2 title change -->
<string name="MessageRecord_you_changed_the_group_name_to_s">You changed the group name to \"%1$s\".</string>
<string name="MessageRecord_s_changed_the_group_name_to_s">%1$s changed the group name to \"%2$s\".</string>
<!-- GV2 avatar change -->
<string name="MessageRecord_you_changed_the_group_avatar">You changed the group avatar.</string>
<string name="MessageRecord_s_changed_the_group_avatar">%1$s changed the group avatar.</string>
<!-- GV2 attribute access level change -->
<string name="MessageRecord_you_changed_who_can_edit_group_info_to_s">You changed who can edit group info to \"%1$s\".</string>
<string name="MessageRecord_s_changed_who_can_edit_group_info_to_s">%1$s changed who can edit group info to \"%2$s\".</string>
<!-- GV2 membership access level change -->
<string name="MessageRecord_you_changed_who_can_edit_group_membership_to_s">You changed who can edit group membership to \"%1$s\".</string>
<string name="MessageRecord_s_changed_who_can_edit_group_membership_to_s">%1$s changed who can edit group membership to \"%2$s\".</string>
<!-- End of GV2 specific update messages -->
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>

View File

@ -0,0 +1,589 @@
package org.thoughtcrime.securesms.database.model;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public final class GroupsV2UpdateMessageProducerTest {
private UUID you;
private UUID alice;
private UUID bob;
private GroupsV2UpdateMessageProducer producer;
@Before
public void setup() {
you = UUID.randomUUID();
alice = UUID.randomUUID();
bob = UUID.randomUUID();
GroupsV2UpdateMessageProducer.DescribeMemberStrategy describeMember = createDescriber(ImmutableMap.of(alice, "Alice", bob, "Bob"));
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), describeMember, you);
}
@Test
public void empty_change() {
DecryptedGroupChange change = changeBy(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice updated the group.")));
}
@Test
public void empty_change_by_you() {
DecryptedGroupChange change = changeBy(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("You updated the group.")));
}
// Member additions
@Test
public void member_added_member() {
DecryptedGroupChange change = changeBy(alice)
.addMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice added Bob.")));
}
@Test
public void you_added_member() {
DecryptedGroupChange change = changeBy(you)
.addMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("You added Bob.")));
}
@Test
public void member_added_you() {
DecryptedGroupChange change = changeBy(alice)
.addMember(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice added you to the group.")));
}
@Test
public void you_added_you() {
DecryptedGroupChange change = changeBy(you)
.addMember(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("You joined the group.")));
}
@Test
public void member_added_themselves() {
DecryptedGroupChange change = changeBy(bob)
.addMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob joined the group.")));
}
// Member removals
@Test
public void member_removed_member() {
DecryptedGroupChange change = changeBy(alice)
.deleteMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice removed Bob.")));
}
@Test
public void you_removed_member() {
DecryptedGroupChange change = changeBy(you)
.deleteMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("You removed Bob.")));
}
@Test
public void member_removed_you() {
DecryptedGroupChange change = changeBy(alice)
.deleteMember(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice removed you from the group.")));
}
@Test
public void you_removed_you() {
DecryptedGroupChange change = changeBy(you)
.deleteMember(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("You left the group.")));
}
@Test
public void member_removed_themselves() {
DecryptedGroupChange change = changeBy(bob)
.deleteMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob left the group.")));
}
// Member role modifications
@Test
public void you_make_member_admin() {
DecryptedGroupChange change = changeBy(you)
.promoteToAdmin(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("You made Alice an admin.")));
}
@Test
public void member_makes_member_admin() {
DecryptedGroupChange change = changeBy(bob)
.promoteToAdmin(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob made Alice an admin.")));
}
@Test
public void member_makes_you_admin() {
DecryptedGroupChange change = changeBy(alice)
.promoteToAdmin(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice made you an admin.")));
}
@Test
public void you_revoked_member_admin() {
DecryptedGroupChange change = changeBy(you)
.demoteToMember(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("You revoked admin privileges from Bob.")));
}
@Test
public void member_revokes_member_admin() {
DecryptedGroupChange change = changeBy(bob)
.demoteToMember(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob revoked admin privileges from Alice.")));
}
@Test
public void member_revokes_your_admin() {
DecryptedGroupChange change = changeBy(alice)
.demoteToMember(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice revoked your admin privileges.")));
}
// Member invitation
@Test
public void you_invited_member() {
DecryptedGroupChange change = changeBy(you)
.invite(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("You invited Alice to the group.")));
}
@Test
public void member_invited_you() {
DecryptedGroupChange change = changeBy(alice)
.invite(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice invited you to the group.")));
}
@Test
public void member_invited_1_person() {
DecryptedGroupChange change = changeBy(alice)
.invite(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice invited 1 person to the group.")));
}
@Test
public void member_invited_2_persons() {
DecryptedGroupChange change = changeBy(alice)
.invite(bob)
.invite(UUID.randomUUID())
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice invited 2 people to the group.")));
}
@Test
public void member_invited_3_persons_and_you() {
DecryptedGroupChange change = changeBy(bob)
.invite(alice)
.invite(you)
.invite(UUID.randomUUID())
.invite(UUID.randomUUID())
.build();
assertThat(producer.describeChange(change), is(Arrays.asList("Bob invited you to the group.", "Bob invited 3 people to the group.")));
}
// Member invitation revocation
@Test
public void member_uninvited_1_person() {
DecryptedGroupChange change = changeBy(alice)
.uninvite(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice revoked an invitation to the group.")));
}
@Test
public void member_uninvited_2_people() {
DecryptedGroupChange change = changeBy(alice)
.uninvite(bob)
.uninvite(UUID.randomUUID())
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice revoked 2 invitations to the group.")));
}
@Test
public void you_uninvited_1_person() {
DecryptedGroupChange change = changeBy(you)
.uninvite(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("You revoked an invitation to the group.")));
}
@Test
public void you_uninvited_2_people() {
DecryptedGroupChange change = changeBy(you)
.uninvite(bob)
.uninvite(UUID.randomUUID())
.build();
assertThat(producer.describeChange(change), is(singletonList("You revoked 2 invitations to the group.")));
}
@Test
public void pending_member_declines_invite() {
DecryptedGroupChange change = changeBy(bob)
.uninvite(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Someone declined an invitation to the group.")));
}
@Test
public void you_decline_invite() {
DecryptedGroupChange change = changeBy(you)
.uninvite(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("You declined the invitation to the group.")));
}
// Promote pending members
@Test
public void member_accepts_invite() {
DecryptedGroupChange change = changeBy(bob)
.promote(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob accepted an invitation to the group.")));
}
@Test
public void you_accept_invite() {
DecryptedGroupChange change = changeBy(you)
.promote(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("You accepted the invitation to the group.")));
}
@Test
public void member_promotes_pending_member() {
DecryptedGroupChange change = changeBy(bob)
.promote(alice)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob added invited member Alice.")));
}
@Test
public void you_promote_pending_member() {
DecryptedGroupChange change = changeBy(you)
.promote(bob)
.build();
assertThat(producer.describeChange(change), is(singletonList("You added invited member Bob.")));
}
@Test
public void member_promotes_you() {
DecryptedGroupChange change = changeBy(bob)
.promote(you)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob added you to the group.")));
}
// Title change
@Test
public void member_changes_title() {
DecryptedGroupChange change = changeBy(alice)
.title("New title")
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice changed the group name to \"New title\".")));
}
@Test
public void you_change_title() {
DecryptedGroupChange change = changeBy(you)
.title("Title 2")
.build();
assertThat(producer.describeChange(change), is(singletonList("You changed the group name to \"Title 2\".")));
}
// Avatar change
@Test
public void member_changes_avatar() {
DecryptedGroupChange change = changeBy(alice)
.avatar("Avatar1")
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice changed the group avatar.")));
}
@Test
public void you_change_avatar() {
DecryptedGroupChange change = changeBy(you)
.avatar("Avatar2")
.build();
assertThat(producer.describeChange(change), is(singletonList("You changed the group avatar.")));
}
// Timer change
@Test
public void member_changes_timer() {
DecryptedGroupChange change = changeBy(bob)
.timer(10)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob set the disappearing message timer to 10 seconds.")));
}
@Test
public void you_change_timer() {
DecryptedGroupChange change = changeBy(you)
.timer(60)
.build();
assertThat(producer.describeChange(change), is(singletonList("You set the disappearing message timer to 1 minute.")));
}
// Attribute access change
@Test
public void member_changes_attribute_access() {
DecryptedGroupChange change = changeBy(bob)
.attributeAccess(AccessControl.AccessRequired.MEMBER)
.build();
assertThat(producer.describeChange(change), is(singletonList("Bob changed who can edit group info to \"All members\".")));
}
@Test
public void you_changed_attribute_access() {
DecryptedGroupChange change = changeBy(you)
.attributeAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertThat(producer.describeChange(change), is(singletonList("You changed who can edit group info to \"Only admins\".")));
}
// Membership access change
@Test
public void member_changes_membership_access() {
DecryptedGroupChange change = changeBy(alice)
.membershipAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertThat(producer.describeChange(change), is(singletonList("Alice changed who can edit group membership to \"Only admins\".")));
}
@Test
public void you_changed_membership_access() {
DecryptedGroupChange change = changeBy(you)
.membershipAccess(AccessControl.AccessRequired.MEMBER)
.build();
assertThat(producer.describeChange(change), is(singletonList("You changed who can edit group membership to \"All members\".")));
}
// Multiple changes
@Test
public void multiple_changes() {
DecryptedGroupChange change = changeBy(alice)
.addMember(bob)
.membershipAccess(AccessControl.AccessRequired.MEMBER)
.title("Title")
.timer(300)
.build();
assertThat(producer.describeChange(change), is(Arrays.asList(
"Alice added Bob.",
"Alice changed the group name to \"Title\".",
"Alice set the disappearing message timer to 5 minutes.",
"Alice changed who can edit group membership to \"All members\".")));
}
private static class ChangeBuilder {
private final DecryptedGroupChange.Builder builder;
ChangeBuilder(@NonNull UUID editor) {
builder = DecryptedGroupChange.newBuilder()
.setEditor(UuidUtil.toByteString(editor));
}
ChangeBuilder addMember(@NonNull UUID newMember) {
builder.addNewMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(newMember)));
return this;
}
ChangeBuilder deleteMember(@NonNull UUID removedMember) {
builder.addDeleteMembers(UuidUtil.toByteString(removedMember));
return this;
}
ChangeBuilder promoteToAdmin(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
ChangeBuilder demoteToMember(@NonNull UUID member) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(member)));
return this;
}
ChangeBuilder invite(@NonNull UUID potentialMember) {
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(potentialMember)));
return this;
}
ChangeBuilder uninvite(@NonNull UUID pendingMember) {
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(pendingMember)));
return this;
}
ChangeBuilder promote(@NonNull UUID pendingMember) {
builder.addPromotePendingMembers(UuidUtil.toByteString(pendingMember));
return this;
}
ChangeBuilder title(@NonNull String newTitle) {
builder.setNewTitle(DecryptedString.newBuilder()
.setValue(newTitle));
return this;
}
ChangeBuilder avatar(@NonNull String newAvatar) {
builder.setNewAvatar(DecryptedString.newBuilder()
.setValue(newAvatar));
return this;
}
ChangeBuilder timer(int duration) {
builder.setNewTimer(DisappearingMessagesTimer.newBuilder()
.setDuration(duration));
return this;
}
ChangeBuilder attributeAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewAttributeAccess(accessRequired);
return this;
}
ChangeBuilder membershipAccess(@NonNull AccessControl.AccessRequired accessRequired) {
builder.setNewMemberAccess(accessRequired);
return this;
}
DecryptedGroupChange build() {
return builder.build();
}
}
private ChangeBuilder changeBy(@NonNull UUID groupEditor) {
return new ChangeBuilder(groupEditor);
}
private @NonNull GroupsV2UpdateMessageProducer.DescribeMemberStrategy createDescriber(@NonNull Map<UUID, String> map) {
return uuid -> {
String name = map.get(uuid);
assertNotNull(name);
return name;
};
}
}