Collapse multiple join request/cancels when from a single person.

This commit is contained in:
Cody Henthorne
2022-03-14 20:49:40 -04:00
parent 216059b659
commit 9d1f46da9f
25 changed files with 736 additions and 41 deletions

View File

@@ -91,7 +91,7 @@ public class GroupDatabase extends Database {
/** Increments with every change to the group */
private static final String V2_REVISION = "revision";
/** Serialized {@link DecryptedGroup} protobuf */
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +

View File

@@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.protobuf.ByteString;
import net.zetetic.database.sqlcipher.SQLiteStatement;
@@ -1093,6 +1094,8 @@ public class SmsDatabase extends MessageDatabase {
@Override
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
boolean tryToCollapseJoinRequestEvents = false;
if (message.isJoined()) {
type = (type & (Types.TOTAL_MASK - Types.BASE_TYPE_MASK)) | Types.JOINED_TYPE;
} else if (message.isPreKeyBundle()) {
@@ -1108,6 +1111,8 @@ public class SmsDatabase extends MessageDatabase {
type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT;
if (incomingGroupUpdateMessage.isJustAGroupLeave()) {
type |= Types.GROUP_LEAVE_BIT;
} else if (incomingGroupUpdateMessage.isCancelJoinRequest()) {
tryToCollapseJoinRequestEvents = true;
}
} else if (incomingGroupUpdateMessage.isUpdate()) {
type |= Types.GROUP_UPDATE_BIT;
@@ -1152,6 +1157,13 @@ public class SmsDatabase extends MessageDatabase {
if (groupRecipient == null) threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
else threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
if (tryToCollapseJoinRequestEvents) {
final Optional<InsertResult> result = collapseJoinRequestEventsIfPossible(threadId, (IncomingGroupUpdateMessage) message);
if (result.isPresent()) {
return result;
}
}
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, message.getSender().serialize());
values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId());
@@ -1809,4 +1821,44 @@ public class SmsDatabase extends MessageDatabase {
}
}
@VisibleForTesting
Optional<InsertResult> collapseJoinRequestEventsIfPossible(long threadId, IncomingGroupUpdateMessage message) {
InsertResult result = null;
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
try {
try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(SignalDatabase.mmsSms().getConversation(threadId, 0, 2))) {
MessageRecord latestMessage = reader.getNext();
if (latestMessage != null && latestMessage.isGroupV2()) {
Optional<ByteString> changeEditor = message.getChangeEditor();
if (changeEditor.isPresent() && latestMessage.isGroupV2JoinRequest(changeEditor.get())) {
String encodedBody;
long id;
MessageRecord secondLatestMessage = reader.getNext();
if (secondLatestMessage != null && secondLatestMessage.isGroupV2JoinRequest(changeEditor.get())) {
id = secondLatestMessage.getId();
encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(secondLatestMessage, message.getChangeRevision(), changeEditor.get());
deleteMessage(latestMessage.getId());
} else {
id = latestMessage.getId();
encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(latestMessage, message.getChangeRevision(), changeEditor.get());
}
ContentValues values = new ContentValues(1);
values.put(BODY, encodedBody);
getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id));
result = new InsertResult(id, threadId);
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return Optional.ofNullable(result);
}
}

View File

@@ -30,10 +30,13 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
final class GroupsV2UpdateMessageProducer {
@@ -639,13 +642,22 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeRequestingMembers(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
Set<ByteString> deleteRequestingUuids = new HashSet<>(change.getDeleteRequestingMembersList());
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), R.drawable.ic_update_group_16));
} else {
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting), R.drawable.ic_update_group_16));
if (deleteRequestingUuids.contains(member.getUuid())) {
updates.add(updateDescription(member.getUuid(), requesting -> context.getResources().getQuantityString(R.plurals.MessageRecord_s_requested_and_cancelled_their_request_to_join_via_the_group_link,
change.getDeleteRequestingMembersCount(),
requesting,
change.getDeleteRequestingMembersCount()), R.drawable.ic_update_group_16));
} else {
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting), R.drawable.ic_update_group_16));
}
}
}
}
@@ -681,9 +693,15 @@ final class GroupsV2UpdateMessageProducer {
}
private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
Set<ByteString> newRequestingUuids = change.getNewRequestingMembersList().stream().map(r -> r.getUuid()).collect(Collectors.toSet());
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (ByteString requestingMember : change.getDeleteRequestingMembersList()) {
if (newRequestingUuids.contains(requestingMember)) {
continue;
}
boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes);
if (requestingMemberIsYou) {

View File

@@ -26,10 +26,12 @@ import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -71,6 +73,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import kotlin.collections.CollectionsKt;
/**
* The base class for message record models that are displayed in
* conversations, as opposed to models that are displayed in a thread list.
@@ -234,7 +238,8 @@ public abstract class MessageRecord extends DisplayRecord {
return selfCreatedGroup(change);
}
private @Nullable DecryptedGroupV2Context getDecryptedGroupV2Context() {
@VisibleForTesting
@Nullable DecryptedGroupV2Context getDecryptedGroupV2Context() {
if (!isGroupUpdate() || !isGroupV2()) {
return null;
}
@@ -409,6 +414,31 @@ public abstract class MessageRecord extends DisplayRecord {
return "";
}
public boolean isGroupV2JoinRequest(ByteString uuid) {
DecryptedGroupV2Context decryptedGroupV2Context = getDecryptedGroupV2Context();
if (decryptedGroupV2Context != null && decryptedGroupV2Context.hasChange()) {
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
return change.getEditor().equals(uuid) && change.getNewRequestingMembersList().stream().anyMatch(r -> r.getUuid().equals(uuid));
}
return false;
}
public static @NonNull String createNewContextWithAppendedDeleteJoinRequest(@NonNull MessageRecord messageRecord, int revision, @NonNull ByteString id) {
DecryptedGroupV2Context decryptedGroupV2Context = messageRecord.getDecryptedGroupV2Context();
if (decryptedGroupV2Context != null && decryptedGroupV2Context.hasChange()) {
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
return Base64.encodeBytes(decryptedGroupV2Context.toBuilder()
.setChange(change.toBuilder()
.setRevision(revision)
.addDeleteRequestingMembers(id))
.build().toByteArray());
}
throw new AssertionError("Attempting to modify a message with no change");
}
/**
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
*/

View File

@@ -2,10 +2,14 @@ package org.thoughtcrime.securesms.sms;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import java.util.Optional;
/**
* Helper util for inspecting GV2 {@link MessageGroupContext} for various message processing.
*/
@@ -42,4 +46,29 @@ public final class GroupV2UpdateMessageUtil {
.build();
return DecryptedGroupUtil.changeIsEmpty(withoutDeletedMembers);
}
public static boolean isJoinRequestCancel(@NonNull MessageGroupContext groupContext) {
if (isGroupV2(groupContext) && isUpdate(groupContext)) {
DecryptedGroupChange decryptedGroupChange = groupContext.requireGroupV2Properties()
.getChange();
return decryptedGroupChange.getDeleteRequestingMembersCount() > 0;
}
return false;
}
public static int getChangeRevision(@NonNull MessageGroupContext groupContext) {
if (isGroupV2(groupContext) && isUpdate(groupContext)) {
return groupContext.requireGroupV2Properties().getChange().getRevision();
}
return -1;
}
public static Optional<ByteString> getChangeEditor(MessageGroupContext groupContext) {
if (isGroupV2(groupContext) && isUpdate(groupContext)) {
return Optional.ofNullable(groupContext.requireGroupV2Properties().getChange().getEditor());
}
return Optional.empty();
}
}

View File

@@ -1,8 +1,12 @@
package org.thoughtcrime.securesms.sms;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import java.util.Optional;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
public final class IncomingGroupUpdateMessage extends IncomingTextMessage {
@@ -43,4 +47,16 @@ public final class IncomingGroupUpdateMessage extends IncomingTextMessage {
public boolean isJustAGroupLeave() {
return GroupV2UpdateMessageUtil.isJustAGroupLeave(groupContext);
}
public boolean isCancelJoinRequest() {
return GroupV2UpdateMessageUtil.isJoinRequestCancel(groupContext);
}
public int getChangeRevision() {
return GroupV2UpdateMessageUtil.getChangeRevision(groupContext);
}
public Optional<ByteString> getChangeEditor() {
return GroupV2UpdateMessageUtil.getChangeEditor(groupContext);
}
}