mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-26 09:27:54 +00:00
Group invite link epoch support.
This commit is contained in:

committed by
Greyson Parrelli

parent
e006306036
commit
477bb45df7
@@ -9,12 +9,14 @@ 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.DecryptedApproveMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
@@ -93,6 +95,10 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeUnknownEditorNewTimer(change, updates);
|
||||
describeUnknownEditorNewAttributeAccess(change, updates);
|
||||
describeUnknownEditorNewMembershipAccess(change, updates);
|
||||
describeUnknownEditorNewGroupInviteLinkAccess(change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeUnknownEditorRequestingMembersApprovals(change, updates);
|
||||
describeUnknownEditorRequestingMembersDeletes(change, updates);
|
||||
|
||||
describeUnknownEditorMemberRemovals(change, updates);
|
||||
|
||||
@@ -112,6 +118,10 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeNewTimer(change, updates);
|
||||
describeNewAttributeAccess(change, updates);
|
||||
describeNewMembershipAccess(change, updates);
|
||||
describeNewGroupInviteLinkAccess(change, updates);
|
||||
describeRequestingMembers(change, updates);
|
||||
describeRequestingMembersApprovals(change, updates);
|
||||
describeRequestingMembersDeletes(change, updates);
|
||||
|
||||
describeMemberRemovals(change, updates);
|
||||
|
||||
@@ -148,7 +158,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
}
|
||||
@@ -157,7 +167,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group, newMember)));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_sharable_group_link, newMember)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
}
|
||||
@@ -498,6 +508,123 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
boolean groupLinkEnabled = false;
|
||||
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link, editor)));
|
||||
}
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link_with_admin_approval)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link_with_admin_approval, editor)));
|
||||
}
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_sharable_group_link, editor)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_sharable_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_sharable_group_link, editor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on)));
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on_with_admin_approval)));
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_off)));
|
||||
break;
|
||||
}
|
||||
|
||||
if (change.getNewInviteLinkPassword().size() > 0) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_reset)));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembers(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_sharable_group_link, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_approved_your_request_to_join_the_group, editor)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember.getUuid(), (editor, requesting) -> context.getString(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved)));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember.getUuid(), requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), requestingMember, (editor, requesting) -> context.getString(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, editor, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
|
||||
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
|
||||
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin)));
|
||||
} else {
|
||||
updates.add(updateDescription(requestingMember, requesting -> context.getString(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DescribeMemberStrategy {
|
||||
|
||||
/**
|
||||
|
@@ -14,6 +14,7 @@ public final class 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);
|
||||
|
@@ -8,7 +8,7 @@ import com.google.protobuf.ByteString;
|
||||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
|
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.groups.v2;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -50,6 +53,10 @@ public final class ProfileKeySet {
|
||||
for (DecryptedMember member : change.getModifiedProfileKeysList()) {
|
||||
addMemberKey(member, editor);
|
||||
}
|
||||
|
||||
for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) {
|
||||
addMemberKey(editor, member.getUuid(), member.getProfileKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +73,14 @@ public final class ProfileKeySet {
|
||||
}
|
||||
|
||||
private void addMemberKey(@NonNull DecryptedMember member, @Nullable UUID changeSource) {
|
||||
UUID memberUuid = UuidUtil.fromByteString(member.getUuid());
|
||||
addMemberKey(changeSource, member.getUuid(), member.getProfileKey());
|
||||
}
|
||||
|
||||
private void addMemberKey(@Nullable UUID changeSource,
|
||||
@NonNull ByteString memberUuidBytes,
|
||||
@NonNull ByteString profileKeyBytes)
|
||||
{
|
||||
UUID memberUuid = UuidUtil.fromByteString(memberUuidBytes);
|
||||
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(memberUuid)) {
|
||||
Log.w(TAG, "Seen unknown member UUID");
|
||||
@@ -75,7 +89,7 @@ public final class ProfileKeySet {
|
||||
|
||||
ProfileKey profileKey;
|
||||
try {
|
||||
profileKey = new ProfileKey(member.getProfileKey().toByteArray());
|
||||
profileKey = new ProfileKey(profileKeyBytes.toByteArray());
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Bad profile key in group");
|
||||
return;
|
||||
|
@@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class Base64UrlSafe {
|
||||
|
||||
private Base64UrlSafe() {
|
||||
}
|
||||
|
||||
public static @NonNull byte[] decode(@NonNull String s) throws IOException {
|
||||
return Base64.decode(s, Base64.URL_SAFE);
|
||||
}
|
||||
|
||||
public static @NonNull byte[] decodePaddingAgnostic(@NonNull String s) throws IOException {
|
||||
switch (s.length() % 4) {
|
||||
case 1:
|
||||
case 3: s = s + "="; break;
|
||||
case 2: s = s + "=="; break;
|
||||
}
|
||||
return decode(s);
|
||||
}
|
||||
|
||||
public static @NonNull String encodeBytes(@NonNull byte[] source) {
|
||||
try {
|
||||
return Base64.encodeBytes(source, Base64.URL_SAFE);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String encodeBytesWithoutPadding(@NonNull byte[] source) {
|
||||
return encodeBytes(source).replace("=", "");
|
||||
}
|
||||
}
|
@@ -463,6 +463,7 @@
|
||||
<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>
|
||||
@@ -914,6 +915,41 @@
|
||||
<string name="MessageRecord_s_changed_who_can_edit_group_membership_to_s">%1$s changed who can edit group membership to \"%2$s\".</string>
|
||||
<string name="MessageRecord_who_can_edit_group_membership_has_been_changed_to_s">Who can edit group membership has been changed to \"%1$s\".</string>
|
||||
|
||||
<!-- GV2 group link invite access level change -->
|
||||
<string name="MessageRecord_you_turned_on_the_sharable_group_link">You turned on the sharable group link.</string>
|
||||
<string name="MessageRecord_you_turned_on_the_sharable_group_link_with_admin_approval">You turned on the sharable group link with admin approval.</string>
|
||||
<string name="MessageRecord_you_turned_off_the_sharable_group_link">You turned off the sharable group link.</string>
|
||||
<string name="MessageRecord_s_turned_on_the_sharable_group_link">%1$s turned on the sharable group link.</string>
|
||||
<string name="MessageRecord_s_turned_on_the_sharable_group_link_with_admin_approval">%1$s turned on the sharable group link with admin approval.</string>
|
||||
<string name="MessageRecord_s_turned_off_the_sharable_group_link">%1$s turned off the sharable group link.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_on">The sharable group link has been turned on.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_on_with_admin_approval">The sharable group link has been turned on with admin approval.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_turned_off">The sharable group link has been turned off.</string>
|
||||
|
||||
<!-- GV2 group link reset -->
|
||||
<string name="MessageRecord_you_reset_the_sharable_group_link">You reset the sharable group link.</string>
|
||||
<string name="MessageRecord_s_reset_the_sharable_group_link">%1$s reset the sharable group link.</string>
|
||||
<string name="MessageRecord_the_sharable_group_link_has_been_reset">The sharable group link has been reset.</string>
|
||||
|
||||
<!-- GV2 group link joins -->
|
||||
<string name="MessageRecord_you_joined_the_group_via_the_sharable_group_link">You joined the group via the sharable group link.</string>
|
||||
<string name="MessageRecord_s_joined_the_group_via_the_sharable_group_link">%1$s joined the group via the sharable group link.</string>
|
||||
|
||||
<!-- GV2 group link requests -->
|
||||
<string name="MessageRecord_you_sent_a_request_to_join_the_group">You sent a request to join the group.</string>
|
||||
<string name="MessageRecord_s_requested_to_join_via_the_sharable_group_link">%1$s requested to join via the sharable group link.</string>
|
||||
|
||||
<!-- GV2 group link approvals -->
|
||||
<string name="MessageRecord_s_approved_your_request_to_join_the_group">%1$s approved your request to join the group.</string>
|
||||
<string name="MessageRecord_s_approved_a_request_to_join_the_group_from_s">%1$s approved a request to join the group from %2$s.</string>
|
||||
<string name="MessageRecord_your_request_to_join_the_group_has_been_approved">Your request to join the group has been approved.</string>
|
||||
<string name="MessageRecord_a_request_to_join_the_group_from_s_has_been_approved">A request to join the group from %1$s has been approved.</string>
|
||||
|
||||
<!-- GV2 group link deny -->
|
||||
<string name="MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin">Your request to join the group has been denied by an admin.</string>
|
||||
<string name="MessageRecord_s_denied_a_request_to_join_the_group_from_s">%1$s denied a request to join the group from %2$s.</string>
|
||||
<string name="MessageRecord_a_request_to_join_the_group_from_s_has_been_denied">A request to join the group from %1$s has been denied.</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>
|
||||
|
@@ -143,7 +143,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
||||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("You joined the group via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -152,7 +152,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group.")));
|
||||
assertThat(describeChange(change), is(singletonList("Bob joined the group via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -209,7 +209,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
||||
.addMember(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(Arrays.asList("You joined the group.", "You added Alice.")));
|
||||
assertThat(describeChange(change), is(Arrays.asList("You joined the group via the sharable group link.", "You added Alice.")));
|
||||
}
|
||||
|
||||
// Member removals
|
||||
@@ -838,6 +838,257 @@ public final class GroupsV2UpdateMessageProducerTest {
|
||||
assertThat(describeChange(change), is(singletonList("Who can edit group membership has been changed to \"Only admins\".")));
|
||||
}
|
||||
|
||||
// Group link access change
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned off the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned off the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_changed_group_link_access_to_any() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_changed_group_link_access_to_administrator_approval() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_turned_off_group_link_access() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been turned off.")));
|
||||
}
|
||||
|
||||
// Group link reset
|
||||
|
||||
@Test
|
||||
public void you_reset_group_link() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You reset the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_reset_group_link() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice reset the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_reset_group_link() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The sharable group link has been reset.")));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the group link is turned on and reset in the same change, assume this is the first time
|
||||
* the link password it being set and do not show reset message.
|
||||
*/
|
||||
@Test
|
||||
public void member_changed_group_link_access_to_on_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ANY)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link.")));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the group link is turned on and reset in the same change, assume this is the first time
|
||||
* the link password it being set and do not show reset message.
|
||||
*/
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_on_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void you_changed_group_link_access_to_off_and_reset() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
|
||||
.resetGroupLink()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(Arrays.asList("You turned off the sharable group link.", "You reset the sharable group link.")));
|
||||
}
|
||||
|
||||
// Group link request
|
||||
|
||||
@Test
|
||||
public void you_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeBy(you)
|
||||
.requestJoin()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You sent a request to join the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.requestJoin()
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob requested to join via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_requested_to_join_the_group() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.requestJoin(alice)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice requested to join via the sharable group link.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_approved_your_join_request() {
|
||||
DecryptedGroupChange change = changeBy(bob)
|
||||
.approveRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Bob approved your request to join the group.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_approved_another_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.approveRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice approved a request to join the group from Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_approved_your_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.approveRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been approved.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_approved_another_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.approveRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been approved.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_denied_another_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.denyRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice denied a request to join the group from Bob.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void member_denied_your_join_request() {
|
||||
DecryptedGroupChange change = changeBy(alice)
|
||||
.denyRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_denied_your_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.denyRequest(you)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Your request to join the group has been denied by an admin.")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknown_denied_another_join_request() {
|
||||
DecryptedGroupChange change = changeByUnknown()
|
||||
.denyRequest(bob)
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("A request to join the group from Bob has been denied.")));
|
||||
}
|
||||
|
||||
// Multiple changes
|
||||
|
||||
@Test
|
||||
|
@@ -1,26 +1,32 @@
|
||||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.DecryptedApproveMember;
|
||||
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.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ChangeBuilder {
|
||||
|
||||
private final DecryptedGroupChange.Builder builder;
|
||||
private final DecryptedGroupChange.Builder builder;
|
||||
@Nullable private final UUID editor;
|
||||
|
||||
public static ChangeBuilder changeBy(@NonNull UUID editor) {
|
||||
return new ChangeBuilder(editor);
|
||||
@@ -31,12 +37,14 @@ public final class ChangeBuilder {
|
||||
}
|
||||
|
||||
ChangeBuilder(@NonNull UUID editor) {
|
||||
builder = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(UuidUtil.toByteString(editor));
|
||||
this.editor = editor;
|
||||
this.builder = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(UuidUtil.toByteString(editor));
|
||||
}
|
||||
|
||||
ChangeBuilder() {
|
||||
builder = DecryptedGroupChange.newBuilder();
|
||||
this.editor = null;
|
||||
this.builder = DecryptedGroupChange.newBuilder();
|
||||
}
|
||||
|
||||
public ChangeBuilder addMember(@NonNull UUID newMember) {
|
||||
@@ -139,7 +147,58 @@ public final class ChangeBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder inviteLinkAccess(@NonNull AccessControl.AccessRequired accessRequired) {
|
||||
builder.setNewInviteLinkAccess(accessRequired);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder resetGroupLink() {
|
||||
builder.setNewInviteLinkPassword(ByteString.copyFrom(GroupLinkPassword.createNew().serialize()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin() {
|
||||
if (editor == null) throw new AssertionError();
|
||||
return requestJoin(editor, newProfileKey());
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull UUID requester) {
|
||||
return requestJoin(requester, newProfileKey());
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull ProfileKey profileKey) {
|
||||
if (editor == null) throw new AssertionError();
|
||||
return requestJoin(editor, profileKey);
|
||||
}
|
||||
|
||||
public ChangeBuilder requestJoin(@NonNull UUID requester, @NonNull ProfileKey profileKey) {
|
||||
builder.addNewRequestingMembers(DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(UuidUtil.toByteString(requester))
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder approveRequest(@NonNull UUID approvedMember) {
|
||||
builder.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.setUuid(UuidUtil.toByteString(approvedMember)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeBuilder denyRequest(@NonNull UUID approvedMember) {
|
||||
builder.addDeleteRequestingMembers(UuidUtil.toByteString(approvedMember));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DecryptedGroupChange build() {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static ProfileKey newProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.util.Base64UrlSafe;
|
||||
import org.whispersystems.util.Base64UrlSafe;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@@ -6,13 +6,11 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.testutil.LogRecorder;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
|
||||
@@ -179,4 +177,29 @@ public final class ProfileKeySetTest {
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(logRecorder.getWarnings(), hasMessages("Bad profile key in group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_editor_is_authoritative() {
|
||||
UUID editor = UUID.randomUUID();
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(profileKey).build());
|
||||
|
||||
assertThat(profileKeySet.getAuthoritativeProfileKeys(), is(Collections.singletonMap(editor, profileKey)));
|
||||
assertTrue(profileKeySet.getProfileKeys().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void new_requesting_member_if_not_editor_is_not_authoritative() {
|
||||
UUID editor = UUID.randomUUID();
|
||||
UUID requesting = UUID.randomUUID();
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
ProfileKeySet profileKeySet = new ProfileKeySet();
|
||||
|
||||
profileKeySet.addKeysFromGroupChange(changeBy(editor).requestJoin(requesting, profileKey).build());
|
||||
|
||||
assertTrue(profileKeySet.getAuthoritativeProfileKeys().isEmpty());
|
||||
assertThat(profileKeySet.getProfileKeys(), is(Collections.singletonMap(requesting, profileKey)));
|
||||
}
|
||||
}
|
||||
|
@@ -1,66 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class Base64UrlSafeTest {
|
||||
|
||||
private final byte[] data;
|
||||
private final String encoded;
|
||||
private final String encodedWithoutPadding;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{ "", "", "" },
|
||||
{ "01", "AQ==", "AQ" },
|
||||
{ "0102", "AQI=", "AQI" },
|
||||
{ "010203", "AQID", "AQID" },
|
||||
{ "030405", "AwQF", "AwQF" },
|
||||
{ "03040506", "AwQFBg==", "AwQFBg" },
|
||||
{ "0304050708", "AwQFBwg=", "AwQFBwg" },
|
||||
{ "af4d6cff", "r01s_w==", "r01s_w" },
|
||||
{ "ffefde", "_-_e", "_-_e" },
|
||||
});
|
||||
}
|
||||
|
||||
public Base64UrlSafeTest(String hexData, String encoded, String encodedWithoutPadding) throws IOException {
|
||||
this.data = Hex.fromStringCondensed(hexData);
|
||||
this.encoded = encoded;
|
||||
this.encodedWithoutPadding = encodedWithoutPadding;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodes_as_expected() {
|
||||
assertEquals(encoded, Base64UrlSafe.encodeBytes(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodes_as_expected_without_padding() {
|
||||
assertEquals(encodedWithoutPadding, Base64UrlSafe.encodeBytesWithoutPadding(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodes_as_expected() throws IOException {
|
||||
assertArrayEquals(data, Base64UrlSafe.decode(encoded));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodes_padding_agnostic_as_expected() throws IOException {
|
||||
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encoded));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodes_as_expected_without_padding() throws IOException {
|
||||
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encodedWithoutPadding));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user