Set profile sharing based on who added you to the group.

master
Alan Evans 2020-09-05 10:09:31 -03:00 committed by Cody Henthorne
parent a870ef0030
commit c797b09228
7 changed files with 155 additions and 55 deletions

View File

@ -1119,19 +1119,19 @@ public class ThreadDatabase extends Database {
Recipient resolved = Recipient.resolved(threadRecipientId);
if (resolved.isPushGroup()) {
if (resolved.isPushV2Group()) {
DecryptedGroup decryptedGroup = DatabaseFactory.getGroupDatabase(context).requireGroup(resolved.requireGroupId().requireV2()).requireV2GroupProperties().getDecryptedGroup();
Optional<UUID> inviter = DecryptedGroupUtil.findInviter(decryptedGroup.getPendingMembersList(), Recipient.self().getUuid().get());
if (inviter.isPresent()) {
RecipientId recipientId = RecipientId.from(inviter.get(), null);
return Extra.forGroupV2invite(recipientId);
} else if (decryptedGroup.getRevision() == 0) {
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(decryptedGroup.getMembersList());
if (foundingMember.isPresent()) {
return Extra.forGroupMessageRequest(RecipientId.from(UuidUtil.fromByteString(foundingMember.get().getUuid()), null));
MessageRecord.InviteAddState inviteAddState = record.getGv2AddInviteState();
if (inviteAddState != null) {
RecipientId from = RecipientId.from(inviteAddState.getAddedOrInvitedBy(), null);
if (inviteAddState.isInvited()) {
Log.i(TAG, "GV2 invite message request from " + from);
return Extra.forGroupV2invite(from);
} else {
Log.i(TAG, "GV2 message request from " + from);
return Extra.forGroupMessageRequest(from);
}
}
Log.w(TAG, "Falling back to unknown message request state for GV2 message");
return Extra.forMessageRequest();
} else {
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());

View File

@ -53,23 +53,24 @@ final class GroupsV2UpdateMessageProducer {
/**
* 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.
* Invitation and revision 0 groups are the most common use cases for this.
* <p>
* When invited, it's possible there's no change available.
* <p>
* When the revision of the group is 0, the change is very noisy and only the editor is useful.
*/
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group) {
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) {
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
if (selfPending.isPresent()) {
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
}
if (group.getRevision() == 0) {
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(group.getMembersList());
if (foundingMember.isPresent()) {
ByteString foundingMemberUuid = foundingMember.get().getUuid();
if (selfUuidBytes.equals(foundingMemberUuid)) {
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
} else {
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
}
ByteString foundingMemberUuid = decryptedGroupChange.getEditor();
if (!foundingMemberUuid.isEmpty()) {
if (selfUuidBytes.equals(foundingMemberUuid)) {
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
} else {
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
}
}

View File

@ -25,6 +25,7 @@ import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.StringUtil;
import org.whispersystems.libsignal.util.guava.Function;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
@ -177,7 +179,7 @@ public abstract class MessageRecord extends DisplayRecord {
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
} else {
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState());
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
}
} catch (IOException e) {
Log.w(TAG, "GV2 Message update detail could not be read", e);
@ -185,6 +187,29 @@ public abstract class MessageRecord extends DisplayRecord {
}
}
public @Nullable InviteAddState getGv2AddInviteState() {
try {
byte[] decoded = Base64.decode(getBody());
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
DecryptedGroup groupState = decryptedGroupV2Context.getGroupState();
boolean invited = DecryptedGroupUtil.findPendingByUuid(groupState.getPendingMembersList(), Recipient.self().requireUuid()).isPresent();
if (decryptedGroupV2Context.hasChange()) {
UUID changeEditor = UuidUtil.fromByteStringOrNull(decryptedGroupV2Context.getChange().getEditor());
if (changeEditor != null) {
return new InviteAddState(invited, changeEditor);
}
}
Log.w(TAG, "GV2 Message editor could not be determined");
return null;
} catch (IOException e) {
Log.w(TAG, "GV2 Message update detail could not be read", e);
return null;
}
}
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
}
@ -378,4 +403,23 @@ public abstract class MessageRecord extends DisplayRecord {
public boolean hasSelfMention() {
return false;
}
public static final class InviteAddState {
private final boolean invited;
private final UUID addedOrInvitedBy;
public InviteAddState(boolean invited, @NonNull UUID addedOrInvitedBy) {
this.invited = invited;
this.addedOrInvitedBy = addedOrInvitedBy;
}
public @NonNull UUID getAddedOrInvitedBy() {
return addedOrInvitedBy;
}
public boolean isInvited() {
return invited;
}
}
}

View File

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
@ -188,7 +189,11 @@ final class GroupManagerV2 {
groupDatabase.onAvatarUpdated(groupId, avatar != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, null, null);
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
.setEditor(UuidUtil.toByteString(selfUuid))
.build();
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, groupChange, null);
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
recipientAndThread.threadId,

View File

@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
@ -209,6 +211,7 @@ public final class GroupsV2StateProcessor {
}
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
determineProfileSharing(inputGroupState, newLocalState);
insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries());
persistLearnedProfileKeys(inputGroupState);
@ -293,30 +296,52 @@ public final class GroupsV2StateProcessor {
jobManager.add(new AvatarGroupsV2DownloadJob(groupId, newLocalState.getAvatar()));
}
boolean fullMemberPostUpdate = GroupProtoUtil.isMember(Recipient.self().getUuid().get(), newLocalState.getMembersList());
boolean trustedAdder = false;
determineProfileSharing(inputGroupState, newLocalState);
}
if (newLocalState.getRevision() == 0) {
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(newLocalState.getMembersList());
private void determineProfileSharing(@NonNull GlobalGroupState inputGroupState,
@NonNull DecryptedGroup newLocalState)
{
if (inputGroupState.getLocalState() != null) {
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), Recipient.self().getUuid().get()).isPresent();
if (foundingMember.isPresent()) {
UUID foundingMemberUuid = UuidUtil.fromByteString(foundingMember.get().getUuid());
Recipient foundingRecipient = Recipient.externalPush(context, foundingMemberUuid, null, false);
if (foundingRecipient.isSystemContact() || foundingRecipient.isProfileSharing()) {
Log.i(TAG, "Group 'adder' is trusted. contact: " + foundingRecipient.isSystemContact() + ", profileSharing: " + foundingRecipient.isProfileSharing());
trustedAdder = true;
}
} else {
Log.i(TAG, "Could not find founding member during gv2 create. Not enabling profile sharing.");
if (wasAMemberAlready) {
Log.i(TAG, "Skipping profile sharing detection as was already a full member before update");
return;
}
}
if (fullMemberPostUpdate && trustedAdder) {
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), Recipient.self().getUuid().get());
if (selfAsMemberOptional.isPresent()) {
DecryptedMember selfAsMember = selfAsMemberOptional.get();
int revisionJoinedAt = selfAsMember.getJoinedAtRevision();
Optional<Recipient> addedByOptional = Stream.of(inputGroupState.getServerHistory())
.map(ServerGroupLogEntry::getChange)
.filter(c -> c != null && c.getRevision() == revisionJoinedAt)
.findFirst()
.map(c -> Optional.fromNullable(UuidUtil.fromByteStringOrNull(c.getEditor()))
.transform(a -> Recipient.externalPush(context, UuidUtil.fromByteStringOrNull(c.getEditor()), null, false)))
.orElse(Optional.absent());
if (addedByOptional.isPresent()) {
Recipient addedBy = addedByOptional.get();
Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId()));
if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact() + ", profileSharing: " + addedBy.isProfileSharing());
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
} else {
Log.i(TAG, "Added to a group, but not enabling profile sharing, as 'adder' is not trusted");
}
} else {
Log.w(TAG, "Could not find founding member during gv2 create. Not enabling profile sharing.");
}
} else {
Log.i(TAG, "Added to a group, but not enabling profile sharing. fullMember: " + fullMemberPostUpdate + ", trustedAdded: " + trustedAdder);
Log.i(TAG, String.format("Added to %s, but not enabling profile sharing as not a fullMember.", groupId));
}
}

View File

@ -1196,12 +1196,36 @@ public final class GroupsV2UpdateMessageProducerTest {
// Group state without a change record
@Test
public void you_created_a_group_change_not_found() {
DecryptedGroup group = newGroupBy(you, 0)
.build();
assertThat(describeNewGroup(group), is("You joined the group."));
}
@Test
public void you_created_a_group() {
DecryptedGroup group = newGroupBy(you, 0)
.build();
assertThat(describeNewGroup(group), is("You created the group."));
DecryptedGroupChange change = changeBy(you)
.addMember(alice)
.addMember(you)
.addMember(bob)
.title("New title")
.build();
assertThat(describeNewGroup(group, change), is("You created the group."));
}
@Test
public void alice_created_a_group_change_not_found() {
DecryptedGroup group = newGroupBy(alice, 0)
.member(you)
.build();
assertThat(describeNewGroup(group), is("You joined the group."));
}
@Test
@ -1210,7 +1234,14 @@ public final class GroupsV2UpdateMessageProducerTest {
.member(you)
.build();
assertThat(describeNewGroup(group), is("Alice added you to the group."));
DecryptedGroupChange change = changeBy(alice)
.addMember(you)
.addMember(alice)
.addMember(bob)
.title("New title")
.build();
assertThat(describeNewGroup(group, change), is("Alice added you to the group."));
}
@Test
@ -1247,8 +1278,12 @@ public final class GroupsV2UpdateMessageProducerTest {
}
private @NonNull String describeNewGroup(@NonNull DecryptedGroup group) {
return describeNewGroup(group, DecryptedGroupChange.getDefaultInstance());
}
private @NonNull String describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange groupChange) {
MainThreadUtil.setMainThread(false);
return producer.describeNewGroup(group).getString();
return producer.describeNewGroup(group, groupChange).getString();
}
private static GroupStateBuilder newGroupBy(UUID foundingMember, int revision) {

View File

@ -160,16 +160,6 @@ public final class DecryptedGroupUtil {
return Optional.absent();
}
public static Optional<DecryptedMember> firstMember(Collection<DecryptedMember> members) {
Iterator<DecryptedMember> iterator = members.iterator();
if (iterator.hasNext()) {
return Optional.of(iterator.next());
} else {
return Optional.absent();
}
}
public static Optional<DecryptedPendingMember> findPendingByUuid(Collection<DecryptedPendingMember> members, UUID uuid) {
ByteString uuidBytes = UuidUtil.toByteString(uuid);