Collapse multiple join request/cancels when from a single person.
This commit is contained in:
parent
216059b659
commit
9d1f46da9f
25 changed files with 736 additions and 41 deletions
|
@ -524,6 +524,7 @@ dependencies {
|
|||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
|
|
|
@ -18,9 +18,9 @@ import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
|
@ -9,7 +9,6 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -34,10 +33,10 @@ import org.thoughtcrime.securesms.util.CursorUtil
|
|||
import org.whispersystems.libsignal.IdentityKey
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.libsignal.state.SessionRecord
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -76,8 +75,6 @@ class RecipientDatabaseTest_merges {
|
|||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
ensureDbEmpty()
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
|
@ -217,13 +214,6 @@ class RecipientDatabaseTest_merges {
|
|||
private val context: Application
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private lateinit var recipients: RecipientDatabase
|
||||
private lateinit var sms: SmsDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private var wallClock: Long = 1000
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipients = SignalDatabase.recipients
|
||||
sms = SignalDatabase.sms
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if no previous messages.
|
||||
*/
|
||||
@Test
|
||||
fun noPreviousMessage() {
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
1,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous message is text.
|
||||
*/
|
||||
@Test
|
||||
fun previousTextMesssage() {
|
||||
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous is unrelated group change.
|
||||
*/
|
||||
@Test
|
||||
fun previousUnrelatedGroupChange() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous join request is from a different recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestFromADifferentRecipient() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = bob,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = bobServiceId) {
|
||||
deleteRequestingMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestCollapse() {
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same, and leave second previous alone if text.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinThenTextCollapse() {
|
||||
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousCollapseAndJoinRequestDoubleCollapse() {
|
||||
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
|
||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
||||
wallClock++
|
||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
||||
}
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
}
|
||||
}
|
|
@ -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, " +
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)}.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1245,6 +1245,11 @@
|
|||
<!-- 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_group_link">%1$s requested to join via the group link.</string>
|
||||
<!-- Update message shown when someone requests to join via group link and cancels the request back to back -->
|
||||
<plurals name="MessageRecord_s_requested_and_cancelled_their_request_to_join_via_the_group_link">
|
||||
<item quantity="one">%1$s requested and cancelled their request to join via the group link.</item>
|
||||
<item quantity="other">%1$s requested and cancelled %2$d requests to join via the group link.</item>
|
||||
</plurals>
|
||||
|
||||
<!-- 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>
|
||||
|
|
|
@ -6,6 +6,8 @@ import leakcanary.LeakCanary
|
|||
import org.signal.spinner.Spinner
|
||||
import org.signal.spinner.Spinner.DatabaseConfig
|
||||
import org.thoughtcrime.securesms.database.DatabaseMonitor
|
||||
import org.thoughtcrime.securesms.database.GV2Transformer
|
||||
import org.thoughtcrime.securesms.database.GV2UpdateTransformer
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
|
@ -37,7 +39,7 @@ class SpinnerApplicationContext : ApplicationContext() {
|
|||
linkedMapOf(
|
||||
"signal" to DatabaseConfig(
|
||||
db = SignalDatabase.rawDatabase,
|
||||
columnTransformers = listOf(MessageBitmaskColumnTransformer)
|
||||
columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer)
|
||||
),
|
||||
"jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase),
|
||||
"keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase),
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
|
||||
object GV2Transformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return columnName == GroupDatabase.V2_DECRYPTED_GROUP || columnName == GroupDatabase.MEMBERS
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
||||
return if (columnName == GroupDatabase.V2_DECRYPTED_GROUP) {
|
||||
val groupBytes = cursor.requireBlob(GroupDatabase.V2_DECRYPTED_GROUP)
|
||||
val group = DecryptedGroup.parseFrom(groupBytes)
|
||||
group.formatAsHtml()
|
||||
} else {
|
||||
val members = cursor.requireString(GroupDatabase.MEMBERS)
|
||||
members?.split(',')?.chunked(20)?.joinToString("<br>") { it.joinToString(",") } ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecryptedGroup.formatAsHtml(): String {
|
||||
return """
|
||||
Revision: $revision
|
||||
Title: $title
|
||||
Avatar: ${(avatar?.length ?: 0) != 0}
|
||||
Timer: ${disappearingMessagesTimer.duration}
|
||||
Description: "$description"
|
||||
Announcement: $isAnnouncementGroup
|
||||
Access: attributes(${accessControl.attributes}) members(${accessControl.members}) link(${accessControl.addFromInviteLink})
|
||||
Members: $membersCount
|
||||
Pending: $pendingMembersCount
|
||||
Requesting: $requestingMembersCount
|
||||
Banned: $bannedMembersCount
|
||||
""".trimIndent().replace("\n", "<br>")
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.signal.spinner.DefaultColumnTransformer
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
|
||||
object GV2UpdateTransformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return columnName == MmsSmsColumns.BODY && (tableName == null || (tableName == SmsDatabase.TABLE_NAME || tableName == MmsDatabase.TABLE_NAME))
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
||||
val type: Long = cursor.getMessageType()
|
||||
|
||||
if (type == -1L) {
|
||||
return DefaultColumnTransformer.transform(tableName, columnName, cursor)
|
||||
}
|
||||
|
||||
val body: String? = CursorUtil.requireString(cursor, MmsSmsColumns.BODY)
|
||||
|
||||
return if (MmsSmsColumns.Types.isGroupV2(type) && MmsSmsColumns.Types.isGroupUpdate(type) && body != null) {
|
||||
val gv2ChangeDescription: UpdateDescription = MessageRecord.getGv2ChangeDescription(ApplicationDependencies.getApplication(), body)
|
||||
gv2ChangeDescription.string
|
||||
} else {
|
||||
body ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.getMessageType(): Long {
|
||||
return when {
|
||||
getColumnIndex(SmsDatabase.TYPE) != -1 -> requireLong(SmsDatabase.TYPE)
|
||||
getColumnIndex(MmsDatabase.MESSAGE_BOX) != -1 -> requireLong(MmsDatabase.MESSAGE_BOX)
|
||||
else -> -1
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
|||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
fun DecryptedGroupChange.Builder.setNewDescription(description: String) {
|
||||
newDescription = DecryptedString.newBuilder().setValue(description).build()
|
||||
|
@ -224,21 +223,3 @@ fun decryptedGroup(
|
|||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun member(serviceId: UUID, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0): DecryptedMember {
|
||||
return member(ServiceId.from(serviceId), role, joinedAt)
|
||||
}
|
||||
|
||||
fun member(serviceId: ServiceId, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0): DecryptedMember {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setRole(role)
|
||||
.setUuid(serviceId.toByteString())
|
||||
.setJoinedAtRevision(joinedAt)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun requestingMember(serviceId: ServiceId): DecryptedRequestingMember {
|
||||
return DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(serviceId.toByteString())
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.junit.Test
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.groups.v2.ChangeBuilder
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class MessageRecordTest_createNewContextWithAppendedDeleteJoinRequest {
|
||||
|
||||
/**
|
||||
* Given a non-gv2 message, when I append, then I expect an assertion error
|
||||
*/
|
||||
@Test(expected = AssertionError::class)
|
||||
fun throwOnNonGv2() {
|
||||
val messageRecord = mock<MessageRecord> {
|
||||
on { decryptedGroupV2Context } doReturn null
|
||||
}
|
||||
|
||||
MessageRecord.createNewContextWithAppendedDeleteJoinRequest(messageRecord, 0, ByteString.EMPTY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a gv2 empty change, when I append, then I expect an assertion error.
|
||||
*/
|
||||
@Test(expected = AssertionError::class)
|
||||
fun throwOnEmptyGv2Change() {
|
||||
val groupContext = DecryptedGroupV2Context.getDefaultInstance()
|
||||
|
||||
val messageRecord = mock<MessageRecord> {
|
||||
on { decryptedGroupV2Context } doReturn groupContext
|
||||
}
|
||||
|
||||
MessageRecord.createNewContextWithAppendedDeleteJoinRequest(messageRecord, 0, ByteString.EMPTY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a gv2 requesting member change, when I append, then I expect new group context including the change with a new delete.
|
||||
*/
|
||||
@Test
|
||||
fun appendDeleteToExistingContext() {
|
||||
val alice = UUID.randomUUID()
|
||||
val aliceByteString = UuidUtil.toByteString(alice)
|
||||
val change = ChangeBuilder.changeBy(alice)
|
||||
.requestJoin(alice)
|
||||
.build()
|
||||
.toBuilder()
|
||||
.setRevision(9)
|
||||
.build()
|
||||
|
||||
val context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder().setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build()
|
||||
|
||||
val messageRecord = mock<MessageRecord> {
|
||||
on { decryptedGroupV2Context } doReturn context
|
||||
}
|
||||
|
||||
val newEncodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(messageRecord, 10, aliceByteString)
|
||||
|
||||
val newContext = DecryptedGroupV2Context.parseFrom(Base64.decode(newEncodedBody))
|
||||
|
||||
assertThat("revision updated to 10", newContext.change.revision, `is`(10))
|
||||
assertThat("change should retain join request", newContext.change.newRequestingMembersList[0].uuid, `is`(aliceByteString))
|
||||
assertThat("change should add delete request", newContext.change.deleteRequestingMembersList[0], `is`(aliceByteString))
|
||||
}
|
||||
|
||||
private fun randomBytes(): ByteArray {
|
||||
val bytes = ByteArray(32)
|
||||
Random().nextBytes(bytes)
|
||||
return bytes
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.SignalStoreRule
|
|||
import org.thoughtcrime.securesms.TestZkGroupServer
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||
import org.thoughtcrime.securesms.database.member
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||
|
|
|
@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.SignalStoreRule
|
|||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.GroupStateTestData
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.member
|
||||
import org.thoughtcrime.securesms.database.requestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.member
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember
|
||||
import org.thoughtcrime.securesms.database.setNewDescription
|
||||
import org.thoughtcrime.securesms.database.setNewTitle
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
|
|
@ -91,6 +91,54 @@ public class GroupV2UpdateMessageUtilTest {
|
|||
assertFalse(isJustAGroupLeave);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isJoinRequestCancel_whenChangeRemovesRequestingMembers_shouldReturnTrue() {
|
||||
// GIVEN
|
||||
UUID alice = UUID.randomUUID();
|
||||
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
|
||||
.denyRequest(alice)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build();
|
||||
|
||||
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
|
||||
|
||||
// WHEN
|
||||
boolean isJoinRequestCancel = GroupV2UpdateMessageUtil.isJoinRequestCancel(messageGroupContext);
|
||||
|
||||
// THEN
|
||||
assertTrue(isJoinRequestCancel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isJoinRequestCancel_whenChangeContainsNoRemoveRequestingMembers_shouldReturnFalse() {
|
||||
// GIVEN
|
||||
UUID alice = UUID.randomUUID();
|
||||
UUID bob = UUID.randomUUID();
|
||||
DecryptedGroupChange change = ChangeBuilder.changeBy(alice)
|
||||
.deleteMember(alice)
|
||||
.addMember(bob)
|
||||
.build();
|
||||
|
||||
DecryptedGroupV2Context context = DecryptedGroupV2Context.newBuilder()
|
||||
.setContext(SignalServiceProtos.GroupContextV2.newBuilder()
|
||||
.setMasterKey(ByteString.copyFrom(randomBytes())))
|
||||
.setChange(change)
|
||||
.build();
|
||||
|
||||
MessageGroupContext messageGroupContext = new MessageGroupContext(context);
|
||||
|
||||
// WHEN
|
||||
boolean isJoinRequestCancel = GroupV2UpdateMessageUtil.isJoinRequestCancel(messageGroupContext);
|
||||
|
||||
// THEN
|
||||
assertFalse(isJoinRequestCancel);
|
||||
}
|
||||
|
||||
private @NonNull byte[] randomBytes() {
|
||||
byte[] bytes = new byte[32];
|
||||
new Random().nextBytes(bytes);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package org.thoughtcrime.securesms.database.model.databaseprotos
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
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.DecryptedRequestingMember
|
||||
import org.signal.zkgroup.groups.GroupMasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.util.UUID
|
||||
|
||||
fun groupContext(masterKey: GroupMasterKey, init: DecryptedGroupV2Context.Builder.() -> Unit): DecryptedGroupV2Context {
|
||||
val builder = DecryptedGroupV2Context.newBuilder()
|
||||
builder.context = encryptedGroupContext(masterKey)
|
||||
builder.init()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun groupChange(editor: ServiceId, init: DecryptedGroupChange.Builder.() -> Unit): DecryptedGroupChange {
|
||||
val builder = DecryptedGroupChange.newBuilder()
|
||||
builder.editor = editor.toByteString()
|
||||
builder.init()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun encryptedGroupContext(masterKey: GroupMasterKey): SignalServiceProtos.GroupContextV2 {
|
||||
return SignalServiceProtos.GroupContextV2.newBuilder().setMasterKey(ByteString.copyFrom(masterKey.serialize())).build()
|
||||
}
|
||||
|
||||
fun DecryptedGroupChange.Builder.addRequestingMember(serviceId: ServiceId) {
|
||||
addNewRequestingMembers(requestingMember(serviceId))
|
||||
}
|
||||
|
||||
fun DecryptedGroupChange.Builder.deleteRequestingMember(serviceId: ServiceId) {
|
||||
addDeleteRequestingMembers(serviceId.toByteString())
|
||||
}
|
||||
|
||||
fun DecryptedGroupChange.Builder.addMember(serviceId: ServiceId) {
|
||||
addNewMembers(member(serviceId))
|
||||
}
|
||||
|
||||
fun ServiceId.toByteString(): ByteString {
|
||||
return UuidUtil.toByteString(uuid())
|
||||
}
|
||||
|
||||
fun member(serviceId: UUID, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0): DecryptedMember {
|
||||
return member(ServiceId.from(serviceId), role, joinedAt)
|
||||
}
|
||||
|
||||
fun member(serviceId: ServiceId, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0): DecryptedMember {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setRole(role)
|
||||
.setUuid(serviceId.toByteString())
|
||||
.setJoinedAtRevision(joinedAt)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun requestingMember(serviceId: ServiceId): DecryptedRequestingMember {
|
||||
return DecryptedRequestingMember.newBuilder()
|
||||
.setUuid(serviceId.toByteString())
|
||||
.build()
|
||||
}
|
|
@ -131,6 +131,7 @@ dependencyResolutionManagement {
|
|||
alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1')
|
||||
alias('espresso-core').to('androidx.test.espresso:espresso-core:3.2.0')
|
||||
alias('mockito-core').to('org.mockito:mockito-inline:4.4.0')
|
||||
alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0')
|
||||
alias('robolectric-robolectric').to('org.robolectric', 'robolectric').versionRef('robolectric')
|
||||
alias('robolectric-shadows-multidex').to('org.robolectric', 'shadows-multidex').versionRef('robolectric')
|
||||
alias('bouncycastle-bcprov-jdk15on').to('org.bouncycastle:bcprov-jdk15on:1.70')
|
||||
|
|
|
@ -3207,6 +3207,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||
<sha256 value="ee52e1c299a632184fba274a9370993e09140429f5e516e6c5570fd6574b297f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.mockito.kotlin" name="mockito-kotlin" version="4.0.0">
|
||||
<artifact name="mockito-kotlin-4.0.0.jar">
|
||||
<sha256 value="046eabba9c38816f75114163ac5074630f335dcdeeac52f228ce71c732c3d75f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="org.mozilla" name="rhino" version="1.7.7">
|
||||
<artifact name="rhino-1.7.7.jar">
|
||||
<sha256 value="73b8d6bbbd1a6a3a87ea0eea301996deac83f8d40b404518a10afd4d320b5b31" origin="Generated by Gradle"/>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
{{#each queryResult.rows}}
|
||||
<tr>
|
||||
{{#each this}}
|
||||
<td>{{{this}}}</td>
|
||||
<td><pre>{{{this}}}</pre></td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
{{#each queryResult.rows}}
|
||||
<tr>
|
||||
{{#each this}}
|
||||
<td>{{{this}}}</td>
|
||||
<td><pre>{{{this}}}</pre></td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.signal.spinner
|
|||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
|
||||
internal object DefaultColumnTransformer : ColumnTransformer {
|
||||
object DefaultColumnTransformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue