Introduce ManyToMany table for group membership.
This commit is contained in:
parent
d635683303
commit
1b7e4e047c
11 changed files with 486 additions and 165 deletions
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
|
@ -48,8 +48,6 @@
|
|||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
|
|
|
@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
|
|||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(
|
||||
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||
destinations = listOf(ContactSearchKey.RecipientSearchKey.Story(myStoryRecipientId))
|
||||
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
||||
)
|
||||
.show(conversationActivity.supportFragmentManager)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.Random
|
||||
|
||||
class GroupTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var groupTable: GroupTable
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
groupTable = SignalDatabase.groups
|
||||
|
||||
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
|
||||
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenICreateGroupV2_thenIExpectMemberRowsPopulated() {
|
||||
val groupId = insertPushGroup()
|
||||
|
||||
//language=sql
|
||||
val members: List<RecipientId> = groupTable.writableDatabase.query(
|
||||
"""
|
||||
SELECT ${GroupTable.MembershipTable.RECIPIENT_ID}
|
||||
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||
WHERE ${GroupTable.MembershipTable.GROUP_ID} = "${groupId.serialize()}"
|
||||
""".trimIndent()
|
||||
).readToList {
|
||||
RecipientId.from(it.requireLong(GroupTable.RECIPIENT_ID))
|
||||
}
|
||||
|
||||
assertEquals(2, members.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupV2_whenIGetGroupsContainingMember_thenIExpectGroup() {
|
||||
val groupId = insertPushGroup()
|
||||
insertThread(groupId)
|
||||
|
||||
val groups = groupTable.getGroupsContainingMember(harness.others[0], false)
|
||||
|
||||
assertEquals(1, groups.size)
|
||||
assertEquals(groupId, groups[0].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnMmsGroup_whenIGetMembers_thenIExpectAllMembers() {
|
||||
val groupId = insertMmsGroup()
|
||||
|
||||
val groups = groupTable.getGroupMemberIds(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF)
|
||||
|
||||
assertEquals(2, groups.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
insertMmsGroup(members = listOf(harness.others[1]))
|
||||
|
||||
val groups = groupTable.queryGroupsByMembership(
|
||||
setOf(harness.self.id, harness.others[1]),
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
)
|
||||
|
||||
assertEquals(2, groups.cursor?.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
insertMmsGroup(members = listOf(harness.others[1]))
|
||||
|
||||
val groups = groupTable.getGroups()
|
||||
|
||||
assertEquals(2, groups.cursor?.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIGetGroup_thenIExpectGroup() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
assertEquals(setOf(harness.self.id, harness.others[0]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndARemap_whenIGetGroup_thenIExpectRemap() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
groupTable.writableDatabase.withinTransaction {
|
||||
RemappedRecords.getInstance().addRecipient(harness.others[0], harness.others[1])
|
||||
}
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
assertEquals(groupRecord.members.toSet(), setOf(harness.self.id, harness.others[1]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||
|
||||
assertTrue(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIRemove_thenIExpectNotAMember() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
groupTable.remove(v2Group, harness.others[0])
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[0])
|
||||
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndNonMember_whenIIsCurrentMember_thenIExpectFalse() {
|
||||
val v2Group = insertPushGroup()
|
||||
|
||||
val actual = groupTable.isCurrentMember(v2Group.requirePush(), harness.others[1])
|
||||
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
|
||||
val v2Group = insertPushGroup()
|
||||
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
|
||||
val groupRecord = groupTable.getGroup(v2Group)
|
||||
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
|
||||
val other = insertMmsGroup(members + listOf(harness.others[1]))
|
||||
val mmsGroup = insertMmsGroup(members)
|
||||
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
|
||||
|
||||
assertNotEquals(other, actual)
|
||||
assertEquals(mmsGroup, actual)
|
||||
}
|
||||
|
||||
private fun insertThread(groupId: GroupId): Long {
|
||||
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
||||
}
|
||||
|
||||
private fun insertMmsGroup(members: List<RecipientId> = listOf(harness.self.id, harness.others[0])): GroupId {
|
||||
val id = GroupId.createMms(SecureRandom())
|
||||
groupTable.create(
|
||||
id,
|
||||
null,
|
||||
members.apply {
|
||||
println("Creating a group with ${members.size} members")
|
||||
}
|
||||
)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
private fun insertPushGroup(): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(
|
||||
listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||
@Test
|
||||
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||
val recipients = harness.others
|
||||
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
|
||||
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||
|
||||
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
|
||||
|
||||
|
@ -42,7 +42,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
|
||||
// GIVEN
|
||||
val recipients = harness.others
|
||||
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
|
||||
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey(it, false) }
|
||||
|
||||
// WHEN
|
||||
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
|
||||
|
@ -59,7 +59,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||
// GIVEN
|
||||
val distributionListMembers = harness.others.take(5)
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
|
||||
// WHEN
|
||||
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
|
||||
|
@ -82,7 +82,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||
val distributionListMembers = harness.others.take(5)
|
||||
val toRemove = distributionListMembers.last()
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
|
@ -108,7 +108,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
|||
// GIVEN
|
||||
val distributionListMembers = harness.others.take(5)
|
||||
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
|
||||
val destinationKey = ContactSearchKey.RecipientSearchKey(SignalDatabase.distributionLists.getRecipientId(distributionList)!!, true)
|
||||
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
|
||||
testScheduler.triggerActions()
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.signal.core.util.SqlUtil.appendArg
|
|||
import org.signal.core.util.SqlUtil.buildArgs
|
||||
import org.signal.core.util.SqlUtil.buildCaseInsensitiveGlobPattern
|
||||
import org.signal.core.util.SqlUtil.buildCollectionQuery
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.exists
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.signal.core.util.logging.Log
|
||||
|
@ -50,7 +51,6 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
|
@ -58,11 +58,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
|
|||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.io.Closeable
|
||||
import java.lang.AssertionError
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.IllegalStateException
|
||||
import java.security.SecureRandom
|
||||
import java.util.ArrayList
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.stream.Collectors
|
||||
|
@ -72,12 +68,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
companion object {
|
||||
private val TAG = Log.tag(GroupTable::class.java)
|
||||
|
||||
const val MEMBER_GROUP_CONCAT = "member_group_concat"
|
||||
|
||||
const val TABLE_NAME = "groups"
|
||||
const val ID = "_id"
|
||||
const val GROUP_ID = "group_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val TITLE = "title"
|
||||
const val MEMBERS = "members"
|
||||
const val AVATAR_ID = "avatar_id"
|
||||
const val AVATAR_KEY = "avatar_key"
|
||||
const val AVATAR_CONTENT_TYPE = "avatar_content_type"
|
||||
|
@ -112,7 +109,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
$GROUP_ID TEXT,
|
||||
$RECIPIENT_ID INTEGER,
|
||||
$TITLE TEXT,
|
||||
$MEMBERS TEXT,
|
||||
$AVATAR_ID INTEGER,
|
||||
$AVATAR_KEY BLOB,
|
||||
$AVATAR_CONTENT_TYPE TEXT,
|
||||
|
@ -145,7 +141,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
GROUP_ID,
|
||||
RECIPIENT_ID,
|
||||
TITLE,
|
||||
MEMBERS,
|
||||
UNMIGRATED_V1_MEMBERS,
|
||||
AVATAR_ID,
|
||||
AVATAR_KEY,
|
||||
|
@ -165,43 +160,77 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
.filterNot { it == RECIPIENT_ID }
|
||||
.map { columnName: String -> "$TABLE_NAME.$columnName" }
|
||||
.toList()
|
||||
|
||||
//language=sql
|
||||
private val JOINED_GROUP_SELECT = """
|
||||
SELECT
|
||||
DISTINCT $TABLE_NAME.*,
|
||||
GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}) as $MEMBER_GROUP_CONCAT
|
||||
FROM $TABLE_NAME
|
||||
INNER JOIN ${MembershipTable.TABLE_NAME} ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||
""".toSingleLine()
|
||||
|
||||
val CREATE_TABLES = arrayOf(CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||
}
|
||||
|
||||
class MembershipTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
|
||||
companion object {
|
||||
const val TABLE_NAME = "group_membership"
|
||||
|
||||
const val ID = "_id"
|
||||
const val GROUP_ID = "group_id"
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
//language=sql
|
||||
@JvmField
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$GROUP_ID TEXT NOT NULL,
|
||||
$RECIPIENT_ID INTEGER NOT NULL,
|
||||
UNIQUE($GROUP_ID, $RECIPIENT_ID)
|
||||
)
|
||||
""".toSingleLine()
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroup(recipientId: RecipientId): Optional<GroupRecord> {
|
||||
readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$RECIPIENT_ID = ?", recipientId)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
getGroup(cursor)
|
||||
} else {
|
||||
Optional.empty()
|
||||
}
|
||||
}
|
||||
return getGroup(SqlUtil.Query("$TABLE_NAME.$RECIPIENT_ID = ?", buildArgs(recipientId)))
|
||||
}
|
||||
|
||||
fun getGroup(groupId: GroupId): Optional<GroupRecord> {
|
||||
return getGroup(SqlUtil.Query("$TABLE_NAME.$GROUP_ID = ?", buildArgs(groupId)))
|
||||
}
|
||||
|
||||
private fun getGroup(query: SqlUtil.Query): Optional<GroupRecord> {
|
||||
//language=sql
|
||||
val select = "$JOINED_GROUP_SELECT WHERE ${query.where} GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}"
|
||||
|
||||
readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$GROUP_ID = ?", groupId.toString())
|
||||
.run()
|
||||
.query(select, query.whereArgs)
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val groupRecord = getGroup(cursor)
|
||||
if (groupRecord.isPresent && RemappedRecords.getInstance().areAnyRemapped(groupRecord.get().members)) {
|
||||
val groupId = groupRecord.get().id
|
||||
val remaps = RemappedRecords.getInstance().buildRemapDescription(groupRecord.get().members)
|
||||
Log.w(TAG, "Found a group with remapped recipients in it's membership list! Updating the list. GroupId: $groupId, Remaps: $remaps", true)
|
||||
|
||||
val remapped: Collection<RecipientId> = RemappedRecords.getInstance().remap(groupRecord.get().members)
|
||||
val oldToNew: List<Pair<RecipientId, RecipientId?>> = groupRecord.get().members.map {
|
||||
it to RemappedRecords.getInstance().getRecipient(it).orElse(null)
|
||||
}.filterNot { (old, new) -> new == null || old == new }
|
||||
|
||||
val updateCount = writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(MEMBERS to remapped.serialize())
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
.run()
|
||||
var updateCount = 0
|
||||
if (oldToNew.isNotEmpty()) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
for ((old, new) in oldToNew) {
|
||||
updateCount += db.update(MembershipTable.TABLE_NAME)
|
||||
.values(MembershipTable.RECIPIENT_ID to new!!.serialize())
|
||||
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, old)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updateCount > 0) {
|
||||
getGroup(groupId)
|
||||
|
@ -240,33 +269,11 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
* @return A gv1 group whose expected v2 ID matches the one provided.
|
||||
*/
|
||||
fun getGroupV1ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
|
||||
readableDatabase
|
||||
.select(*GROUP_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$EXPECTED_V2_ID = ?", gv2Id)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
getGroup(cursor)
|
||||
} else {
|
||||
Optional.empty()
|
||||
}
|
||||
}
|
||||
return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ?", buildArgs(gv2Id)))
|
||||
}
|
||||
|
||||
fun getGroupByDistributionId(distributionId: DistributionId): Optional<GroupRecord> {
|
||||
readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$DISTRIBUTION_ID = ?", distributionId)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
getGroup(cursor)
|
||||
} else {
|
||||
Optional.empty()
|
||||
}
|
||||
}
|
||||
return getGroup(SqlUtil.Query("$TABLE_NAME.$DISTRIBUTION_ID = ?", buildArgs(distributionId)))
|
||||
}
|
||||
|
||||
fun removeUnmigratedV1Members(id: GroupId.V2) {
|
||||
|
@ -338,7 +345,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
|
||||
fun queryGroupsByTitle(inputQuery: String, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
|
||||
val query = getGroupQueryWhereStatement(inputQuery, includeInactive, excludeV1, excludeMms)
|
||||
val cursor = databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, "$TITLE COLLATE NOCASE ASC")
|
||||
val statement = """
|
||||
$JOINED_GROUP_SELECT
|
||||
WHERE ${query.where}
|
||||
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||
ORDER BY $TITLE COLLATE NOCASE ASC
|
||||
""".trimIndent()
|
||||
|
||||
val cursor = databaseHelper.signalReadableDatabase.query(statement, query.whereArgs)
|
||||
return Reader(cursor)
|
||||
}
|
||||
|
||||
|
@ -353,21 +367,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
recipientIds = recipientIds.take(30).toSet()
|
||||
}
|
||||
|
||||
val recipientLikeClauses = recipientIds
|
||||
.map { it.toLong() }
|
||||
.map { id -> "($MEMBERS LIKE $id || ',%' OR $MEMBERS LIKE '%,' || $id || ',%' OR $MEMBERS LIKE '%,' || $id)" }
|
||||
.toList()
|
||||
val membershipQuery = SqlUtil.buildSingleCollectionQuery("${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}", recipientIds)
|
||||
|
||||
var query: String
|
||||
val queryArgs: Array<String>
|
||||
val membershipQuery = "(" + Util.join(recipientLikeClauses, " OR ") + ")"
|
||||
|
||||
if (includeInactive) {
|
||||
query = "$membershipQuery AND ($ACTIVE = ? OR $RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||
queryArgs = buildArgs(1)
|
||||
query = "${membershipQuery.where} AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||
queryArgs = membershipQuery.whereArgs + buildArgs(1)
|
||||
} else {
|
||||
query = "$membershipQuery AND $ACTIVE = ?"
|
||||
queryArgs = buildArgs(1)
|
||||
query = "${membershipQuery.where} AND $ACTIVE = ?"
|
||||
queryArgs = membershipQuery.whereArgs + buildArgs(1)
|
||||
}
|
||||
|
||||
if (excludeV1) {
|
||||
|
@ -378,15 +388,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
query += " AND $MMS = 0"
|
||||
}
|
||||
|
||||
return Reader(readableDatabase.query(TABLE_NAME, null, query, queryArgs, null, null, null))
|
||||
return Reader(readableDatabase.query("$JOINED_GROUP_SELECT WHERE $query GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}", queryArgs))
|
||||
}
|
||||
|
||||
private fun queryGroupsByRecency(groupQuery: GroupQuery): Reader {
|
||||
val query = getGroupQueryWhereStatement(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
|
||||
val sql = """
|
||||
SELECT $TABLE_NAME.*
|
||||
FROM $TABLE_NAME LEFT JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
||||
$JOINED_GROUP_SELECT
|
||||
WHERE ${query.where}
|
||||
${"GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}"}
|
||||
ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC
|
||||
""".toSingleLine()
|
||||
|
||||
|
@ -407,7 +417,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern(inputQuery)
|
||||
|
||||
if (includeInactive) {
|
||||
query = "$TITLE GLOB ? AND ($ACTIVE = ? OR $RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||
query = "$TITLE GLOB ? AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))"
|
||||
queryArgs = buildArgs(caseInsensitiveQuery, 1)
|
||||
} else {
|
||||
query = "$TITLE GLOB ? AND $ACTIVE = ?"
|
||||
|
@ -456,23 +466,27 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
}
|
||||
|
||||
fun getOrCreateMmsGroupForMembers(members: List<RecipientId>): GroupId.Mms {
|
||||
val sortedMembers = members.sorted()
|
||||
fun getOrCreateMmsGroupForMembers(members: Set<RecipientId>): GroupId.Mms {
|
||||
//language=sql
|
||||
val statement = """
|
||||
SELECT ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} as gid
|
||||
FROM ${MembershipTable.TABLE_NAME}
|
||||
INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
|
||||
WHERE ${MembershipTable.TABLE_NAME}.$RECIPIENT_ID IN (${members.joinToString(",") { it.serialize() }}) AND $TABLE_NAME.$MMS = 1
|
||||
GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}
|
||||
HAVING (SELECT COUNT(*) FROM ${MembershipTable.TABLE_NAME} WHERE ${MembershipTable.GROUP_ID} = gid) = ${members.size}
|
||||
ORDER BY ${MembershipTable.TABLE_NAME}.${MembershipTable.ID} ASC
|
||||
""".toSingleLine()
|
||||
|
||||
readableDatabase
|
||||
.select(GROUP_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$MEMBERS = ? AND $MMS = ?", sortedMembers.serialize(), 1)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToNext()) {
|
||||
GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)).requireMms()
|
||||
} else {
|
||||
val groupId = GroupId.createMms(SecureRandom())
|
||||
create(groupId, null, sortedMembers)
|
||||
groupId
|
||||
}
|
||||
return readableDatabase.query(statement).use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
return GroupId.parseOrThrow(cursor.requireNonNullString("gid")).requireMms()
|
||||
} else {
|
||||
val groupId = GroupId.createMms(SecureRandom())
|
||||
create(groupId, null, members)
|
||||
groupId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -493,9 +507,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
|
||||
@WorkerThread
|
||||
fun getGroupsContainingMember(recipientId: RecipientId, pushOnly: Boolean, includeInactive: Boolean): List<GroupRecord> {
|
||||
val table = "$TABLE_NAME INNER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}"
|
||||
var query = "$MEMBERS LIKE ?"
|
||||
var args = buildArgs("%${recipientId.serialize()}%")
|
||||
//language=sql
|
||||
val table = """
|
||||
$JOINED_GROUP_SELECT
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
|
||||
""".toSingleLine()
|
||||
|
||||
var query = "${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} = ?"
|
||||
var args = buildArgs(recipientId)
|
||||
val orderBy = "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC"
|
||||
|
||||
if (pushOnly) {
|
||||
|
@ -509,23 +528,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
return readableDatabase
|
||||
.query(table, null, query, args, null, null, orderBy)
|
||||
.query("$table WHERE $query GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} ORDER BY $orderBy".apply { println(this) }, args)
|
||||
.readToList { cursor ->
|
||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
||||
if (RecipientId.serializedListContains(serializedMembers, recipientId)) {
|
||||
getGroup(cursor).get()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
getGroup(cursor).get()
|
||||
}
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
fun getGroups(): Reader {
|
||||
val cursor = readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.run()
|
||||
val cursor = readableDatabase.query("$JOINED_GROUP_SELECT GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID}")
|
||||
return Reader(cursor)
|
||||
}
|
||||
|
||||
|
@ -648,6 +658,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
groupMasterKey: GroupMasterKey?,
|
||||
groupState: DecryptedGroup?
|
||||
) {
|
||||
val membershipValues = mutableListOf<ContentValues>()
|
||||
val groupRecipientId = recipients.getOrInsertFromGroupId(groupId)
|
||||
val members: List<RecipientId> = memberCollection.toSet().sorted()
|
||||
var groupMembers: List<RecipientId> = members
|
||||
|
@ -657,7 +668,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
values.put(RECIPIENT_ID, groupRecipientId.serialize())
|
||||
values.put(GROUP_ID, groupId.toString())
|
||||
values.put(TITLE, title)
|
||||
values.put(MEMBERS, members.serialize())
|
||||
membershipValues.addAll(members.toContentValues(groupId))
|
||||
values.put(MMS, groupId.isMms)
|
||||
|
||||
if (avatar != null) {
|
||||
|
@ -693,14 +704,25 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
values.put(V2_MASTER_KEY, groupMasterKey.serialize())
|
||||
values.put(V2_REVISION, groupState.revision)
|
||||
values.put(V2_DECRYPTED_GROUP, groupState.toByteArray())
|
||||
values.put(MEMBERS, groupMembers.serialize())
|
||||
membershipValues.clear()
|
||||
membershipValues.addAll(groupMembers.toContentValues(groupId))
|
||||
} else {
|
||||
if (groupId.isV2) {
|
||||
throw AssertionError("V2 group id but no master key")
|
||||
}
|
||||
}
|
||||
|
||||
writableDatabase.insert(TABLE_NAME, null, values)
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.insert(TABLE_NAME, null, values)
|
||||
SqlUtil.buildBulkInsert(
|
||||
MembershipTable.TABLE_NAME,
|
||||
arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID),
|
||||
membershipValues
|
||||
)
|
||||
.forEach {
|
||||
db.execSQL(it.where, it.whereArgs)
|
||||
}
|
||||
}
|
||||
|
||||
if (groupState != null && groupState.hasDisappearingMessagesTimer()) {
|
||||
recipients.setExpireMessages(groupRecipientId, groupState.disappearingMessagesTimer.duration)
|
||||
|
@ -829,7 +851,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
val groupMembers = getV2GroupMembers(decryptedGroup, true)
|
||||
contentValues.put(MEMBERS, groupMembers.serialize())
|
||||
|
||||
if (existingGroup.isPresent && existingGroup.get().isV2Group) {
|
||||
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
|
||||
|
@ -842,11 +863,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
}
|
||||
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(contentValues)
|
||||
.where("$GROUP_ID = ?", groupId.toString())
|
||||
.run()
|
||||
writableDatabase.withinTransaction { database ->
|
||||
database
|
||||
.update(TABLE_NAME)
|
||||
.values(contentValues)
|
||||
.where("$GROUP_ID = ?", groupId.toString())
|
||||
.run()
|
||||
|
||||
performMembershipUpdate(database, groupId, groupMembers)
|
||||
}
|
||||
|
||||
if (decryptedGroup.hasDisappearingMessagesTimer()) {
|
||||
recipients.setExpireMessages(groupRecipientId, decryptedGroup.disappearingMessagesTimer.duration)
|
||||
|
@ -898,27 +923,24 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
fun updateMembers(groupId: GroupId, members: List<RecipientId>) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
MEMBERS to members.sorted().serialize(),
|
||||
ACTIVE to 1
|
||||
)
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
.run()
|
||||
writableDatabase.withinTransaction { database ->
|
||||
database
|
||||
.update(TABLE_NAME)
|
||||
.values(ACTIVE to 1)
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
.run()
|
||||
|
||||
performMembershipUpdate(database, groupId, members)
|
||||
}
|
||||
|
||||
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
||||
Recipient.live(groupRecipient).refresh()
|
||||
}
|
||||
|
||||
fun remove(groupId: GroupId, source: RecipientId) {
|
||||
val currentMembers: MutableList<RecipientId> = getCurrentMembers(groupId)
|
||||
currentMembers -= source
|
||||
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(MEMBERS to currentMembers.serialize())
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
.delete(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, source)
|
||||
.run()
|
||||
|
||||
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
|
||||
|
@ -927,17 +949,34 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
|
||||
private fun getCurrentMembers(groupId: GroupId): MutableList<RecipientId> {
|
||||
return readableDatabase
|
||||
.select(MEMBERS)
|
||||
.from(TABLE_NAME)
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
.select(MembershipTable.RECIPIENT_ID)
|
||||
.from(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.GROUP_ID} = ?", groupId)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
||||
return RecipientId.fromSerializedList(serializedMembers)
|
||||
RecipientId.from(cursor.requireLong(MembershipTable.RECIPIENT_ID))
|
||||
}
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
private fun performMembershipUpdate(database: SQLiteDatabase, groupId: GroupId, members: Collection<RecipientId>) {
|
||||
check(database.inTransaction())
|
||||
database
|
||||
.delete(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.GROUP_ID} = ?", groupId)
|
||||
.run()
|
||||
|
||||
val inserts = SqlUtil.buildBulkInsert(
|
||||
MembershipTable.TABLE_NAME,
|
||||
arrayOf(MembershipTable.GROUP_ID, MembershipTable.RECIPIENT_ID),
|
||||
members.toContentValues(groupId)
|
||||
)
|
||||
|
||||
inserts.forEach {
|
||||
database.execSQL(it.where, it.whereArgs)
|
||||
}
|
||||
}
|
||||
|
||||
fun isActive(groupId: GroupId): Boolean {
|
||||
val record = getGroup(groupId)
|
||||
return record.isPresent && record.get().isActive
|
||||
|
@ -961,19 +1000,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
|
||||
@WorkerThread
|
||||
fun isCurrentMember(groupId: Push, recipientId: RecipientId): Boolean {
|
||||
readableDatabase
|
||||
.select(MEMBERS)
|
||||
.from(TABLE_NAME)
|
||||
.where("$GROUP_ID = ?", groupId)
|
||||
return readableDatabase
|
||||
.exists(MembershipTable.TABLE_NAME)
|
||||
.where("${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ?", groupId, recipientId)
|
||||
.run()
|
||||
.use { cursor ->
|
||||
return if (cursor.moveToFirst()) {
|
||||
val serializedMembers = cursor.requireNonNullString(MEMBERS)
|
||||
RecipientId.serializedListContains(serializedMembers, recipientId)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllGroupV2Ids(): List<GroupId.V2> {
|
||||
|
@ -1005,15 +1035,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
}
|
||||
|
||||
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
||||
writableDatabase
|
||||
.update(MembershipTable.TABLE_NAME)
|
||||
.values(RECIPIENT_ID to toId.serialize())
|
||||
.where("${MembershipTable.RECIPIENT_ID} = ?", fromId)
|
||||
.run()
|
||||
|
||||
for (group in getGroupsContainingMember(fromId, false, true)) {
|
||||
val newMembers: Set<RecipientId> = group.members.toSet() - fromId + toId
|
||||
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(MEMBERS to newMembers.serialize())
|
||||
.where("$RECIPIENT_ID = ?", group.recipientId)
|
||||
.run()
|
||||
|
||||
if (group.isV2Group) {
|
||||
removeUnmigratedV1Members(group.id.requireV2(), listOf(fromId))
|
||||
}
|
||||
|
@ -1042,7 +1070,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
id = GroupId.parseOrThrow(cursor.requireNonNullString(GROUP_ID)),
|
||||
recipientId = RecipientId.from(cursor.requireNonNullString(RECIPIENT_ID)),
|
||||
title = cursor.requireString(TITLE),
|
||||
serializedMembers = cursor.requireString(MEMBERS),
|
||||
serializedMembers = cursor.requireString(MEMBER_GROUP_CONCAT),
|
||||
serializedUnmigratedV1Members = cursor.requireString(UNMIGRATED_V1_MEMBERS),
|
||||
avatarId = cursor.requireLong(AVATAR_ID),
|
||||
avatarKey = cursor.requireBlob(AVATAR_KEY),
|
||||
|
@ -1252,6 +1280,15 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
return RecipientId.toSerializedList(this)
|
||||
}
|
||||
|
||||
private fun Collection<RecipientId>.toContentValues(groupId: GroupId): List<ContentValues> {
|
||||
return map {
|
||||
contentValuesOf(
|
||||
MembershipTable.GROUP_ID to groupId.serialize(),
|
||||
MembershipTable.RECIPIENT_ID to it.serialize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uuidsToRecipientIds(uuids: List<UUID>): MutableList<RecipientId> {
|
||||
return uuids
|
||||
.asSequence()
|
||||
|
|
|
@ -86,7 +86,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
|||
db.execSQL(IdentityTable.CREATE_TABLE)
|
||||
db.execSQL(DraftTable.CREATE_TABLE)
|
||||
db.execSQL(PushTable.CREATE_TABLE)
|
||||
db.execSQL(GroupTable.CREATE_TABLE)
|
||||
executeStatements(db, GroupTable.CREATE_TABLES)
|
||||
db.execSQL(RecipientTable.CREATE_TABLE)
|
||||
db.execSQL(GroupReceiptTable.CREATE_TABLE)
|
||||
db.execSQL(OneTimePreKeyTable.CREATE_TABLE)
|
||||
|
|
|
@ -1610,12 +1610,17 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
private fun createQuery(where: String, orderBy: String, offset: Long, limit: Long): String {
|
||||
val projection = COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION.joinToString(separator = ",")
|
||||
|
||||
//language=sql
|
||||
var query = """
|
||||
SELECT $projection
|
||||
SELECT $projection, ${GroupTable.MEMBER_GROUP_CONCAT}
|
||||
FROM $TABLE_NAME
|
||||
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
|
||||
LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
||||
WHERE $where
|
||||
LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$RECIPIENT_ID = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
|
||||
LEFT OUTER JOIN (
|
||||
SELECT group_id, GROUP_CONCAT(${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID}) as ${GroupTable.MEMBER_GROUP_CONCAT}
|
||||
FROM ${GroupTable.MembershipTable.TABLE_NAME}
|
||||
) as MembershipAlias ON MembershipAlias.${GroupTable.MembershipTable.GROUP_ID} = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
|
||||
WHERE $where
|
||||
ORDER BY $orderBy
|
||||
""".trimIndent()
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V168_SingleMessageT
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V169_EmojiSearchIndexRank
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V170_CallTableMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V171_ThreadForeignKeyFix
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V172_GroupMembershipMigration
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -35,7 +36,7 @@ object SignalDatabaseMigrations {
|
|||
|
||||
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
||||
|
||||
const val DATABASE_VERSION = 171
|
||||
const val DATABASE_VERSION = 172
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
@ -130,6 +131,10 @@ object SignalDatabaseMigrations {
|
|||
if (oldVersion < 171) {
|
||||
V171_ThreadForeignKeyFix.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
if (oldVersion < 172) {
|
||||
V172_GroupMembershipMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireNonNullString
|
||||
|
||||
/**
|
||||
* Migrates all IDs from the GroupTable into the GroupMembershipTable
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V172_GroupMembershipMigration : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE group_membership (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
group_id TEXT NOT NULL,
|
||||
recipient_id INTEGER NOT NULL,
|
||||
UNIQUE(group_id, recipient_id)
|
||||
);
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
//language=sql
|
||||
val total = db.query("SELECT COUNT(*) FROM groups").use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getInt(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
(0..total).chunked(500).forEachIndexed { index, _ ->
|
||||
//language=sql
|
||||
val groupIdToMembers: List<Pair<String, List<Long>>> = db.query("SELECT members, group_id FROM groups LIMIT 500 OFFSET ${index * 500}").readToList { cursor ->
|
||||
val groupId = cursor.requireNonNullString("group_id")
|
||||
val members: List<Long> = cursor.requireNonNullString("members").split(",").filterNot { it.isEmpty() }.map { it.toLong() }
|
||||
|
||||
groupId to members
|
||||
}
|
||||
|
||||
for ((group_id, members) in groupIdToMembers) {
|
||||
val queries = SqlUtil.buildBulkInsert(
|
||||
"group_membership",
|
||||
arrayOf("group_id", "recipient_id"),
|
||||
members.map {
|
||||
contentValuesOf(
|
||||
"group_id" to group_id,
|
||||
"recipient_id" to it
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
for (query in queries) {
|
||||
db.execSQL(query.where, query.whereArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.execSQL("ALTER TABLE groups DROP COLUMN members")
|
||||
}
|
||||
}
|
|
@ -245,7 +245,7 @@ public class MmsDownloadJob extends BaseJob {
|
|||
}
|
||||
|
||||
if (members.size() > 2) {
|
||||
List<RecipientId> recipients = new ArrayList<>(members);
|
||||
Set<RecipientId> recipients = new HashSet<>(members);
|
||||
group = Optional.of(SignalDatabase.groups().getOrCreateMmsGroupForMembers(recipients));
|
||||
}
|
||||
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, TimeUnit.SECONDS.toMillis(retrieved.getDate()), -1, System.currentTimeMillis(), attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts), false, false);
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database
|
|||
import android.database.Cursor
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
|
@ -14,7 +13,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil
|
|||
|
||||
object GV2Transformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return columnName == GroupTable.V2_DECRYPTED_GROUP || columnName == GroupTable.MEMBERS
|
||||
return columnName == GroupTable.V2_DECRYPTED_GROUP
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
|
||||
|
@ -23,8 +22,7 @@ object GV2Transformer : ColumnTransformer {
|
|||
val group = DecryptedGroup.parseFrom(groupBytes)
|
||||
group.formatAsHtml()
|
||||
} else {
|
||||
val members = cursor.requireString(GroupTable.MEMBERS)
|
||||
members?.split(',')?.chunked(20)?.joinToString("<br>") { it.joinToString(",") } ?: ""
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue