2023-01-02 04:05:00 +00:00
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.text.TextUtils
import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf
import org.intellij.lang.annotations.Language
import org.signal.core.util.SetUtil
import org.signal.core.util.SqlUtil
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
2023-01-24 13:59:01 +00:00
import org.signal.core.util.delete
2023-01-02 04:05:00 +00:00
import org.signal.core.util.exists
import org.signal.core.util.isAbsent
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalString
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleInt
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.toSingleLine
import org.signal.core.util.update
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.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupId.Push
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
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.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
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.security.SecureRandom
import java.util.Optional
import java.util.UUID
import java.util.stream.Collectors
class GroupTable ( context : Context ? , databaseHelper : SignalDatabase ? ) : DatabaseTable ( context , databaseHelper ) , RecipientIdDatabaseReference {
companion object {
private val TAG = Log . tag ( GroupTable :: class . java )
2023-01-24 13:59:01 +00:00
const val MEMBER _GROUP _CONCAT = " member_group_concat "
2023-01-26 18:10:38 +00:00
const val THREAD _DATE = " thread_date "
2023-01-24 13:59:01 +00:00
2023-01-02 04:05:00 +00:00
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 AVATAR _ID = " avatar_id "
const val AVATAR _KEY = " avatar_key "
const val AVATAR _CONTENT _TYPE = " avatar_content_type "
const val AVATAR _RELAY = " avatar_relay "
const val AVATAR _DIGEST = " avatar_digest "
const val TIMESTAMP = " timestamp "
const val ACTIVE = " active "
const val MMS = " mms "
const val EXPECTED _V2 _ID = " expected_v2_id "
const val UNMIGRATED _V1 _MEMBERS = " former_v1_members "
const val DISTRIBUTION _ID = " distribution_id "
const val SHOW _AS _STORY _STATE = " display_as_story "
const val LAST _FORCE _UPDATE _TIMESTAMP = " last_force_update_timestamp "
/** 32 bytes serialized [GroupMasterKey] */
const val V2 _MASTER _KEY = " master_key "
/** Increments with every change to the group */
const val V2 _REVISION = " revision "
/** Serialized [DecryptedGroup] protobuf */
const val V2 _DECRYPTED _GROUP = " decrypted_group "
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
@Deprecated ( " " )
private val AUTH _SERVICE _ID = " auth_service_id "
@JvmField
val CREATE _TABLE = """
CREATE TABLE $ TABLE _NAME (
$ ID INTEGER PRIMARY KEY ,
$ GROUP _ID TEXT ,
$ RECIPIENT _ID INTEGER ,
$ TITLE TEXT ,
$ AVATAR _ID INTEGER ,
$ AVATAR _KEY BLOB ,
$ AVATAR _CONTENT _TYPE TEXT ,
$ AVATAR _RELAY TEXT ,
$ TIMESTAMP INTEGER ,
$ ACTIVE INTEGER DEFAULT 1 ,
$ AVATAR _DIGEST BLOB ,
$ MMS INTEGER DEFAULT 0 ,
$ V2 _MASTER _KEY BLOB ,
$ V2 _REVISION BLOB ,
$ V2 _DECRYPTED _GROUP BLOB ,
$ EXPECTED _V2 _ID TEXT DEFAULT NULL ,
$ UNMIGRATED _V1 _MEMBERS TEXT DEFAULT NULL ,
$ DISTRIBUTION _ID TEXT DEFAULT NULL ,
$ SHOW _AS _STORY _STATE INTEGER DEFAULT 0 ,
$ AUTH _SERVICE _ID TEXT DEFAULT NULL ,
$ LAST _FORCE _UPDATE _TIMESTAMP INTEGER DEFAULT 0
)
"""
@JvmField
val CREATE _INDEXS = arrayOf (
" CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON $TABLE _NAME ( $GROUP _ID); " ,
" CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON $TABLE _NAME ( $RECIPIENT _ID); " ,
" CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON $TABLE _NAME ( $EXPECTED _V2_ID); " ,
" CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON $TABLE _NAME( $DISTRIBUTION _ID); "
)
private val GROUP _PROJECTION = arrayOf (
GROUP _ID ,
RECIPIENT _ID ,
TITLE ,
UNMIGRATED _V1 _MEMBERS ,
AVATAR _ID ,
AVATAR _KEY ,
AVATAR _CONTENT _TYPE ,
AVATAR _RELAY ,
AVATAR _DIGEST ,
TIMESTAMP ,
ACTIVE ,
MMS ,
V2 _MASTER _KEY ,
V2 _REVISION ,
V2 _DECRYPTED _GROUP ,
LAST _FORCE _UPDATE _TIMESTAMP
)
val TYPED _GROUP _PROJECTION = GROUP _PROJECTION
. filterNot { it == RECIPIENT _ID }
. map { columnName : String -> " $TABLE _NAME. $columnName " }
. toList ( )
2023-01-24 13:59:01 +00:00
//language=sql
private val JOINED _GROUP _SELECT = """
SELECT
DISTINCT $ TABLE_NAME . * ,
2023-01-27 18:33:03 +00:00
(
SELECT GROUP _CONCAT ( $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } )
FROM $ { MembershipTable . TABLE _NAME }
WHERE $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
) as $ MEMBER _GROUP _CONCAT
2023-01-24 13:59:01 +00:00
FROM $ TABLE _NAME
""" .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()
}
2023-01-02 04:05:00 +00:00
}
fun getGroup ( recipientId : RecipientId ) : Optional < GroupRecord > {
2023-01-24 13:59:01 +00:00
return getGroup ( SqlUtil . Query ( " $TABLE _NAME. $RECIPIENT _ID = ? " , buildArgs ( recipientId ) ) )
2023-01-02 04:05:00 +00:00
}
fun getGroup ( groupId : GroupId ) : Optional < GroupRecord > {
2023-01-24 13:59:01 +00:00
return getGroup ( SqlUtil . Query ( " $TABLE _NAME. $GROUP _ID = ? " , buildArgs ( groupId ) ) )
}
private fun getGroup ( query : SqlUtil . Query ) : Optional < GroupRecord > {
//language=sql
2023-01-27 18:33:03 +00:00
val select = " $JOINED _GROUP_SELECT WHERE ${query.where} "
2023-01-24 13:59:01 +00:00
2023-01-02 04:05:00 +00:00
readableDatabase
2023-01-24 13:59:01 +00:00
. query ( select , query . whereArgs )
2023-01-02 04:05:00 +00:00
. use { cursor ->
return if ( cursor . moveToFirst ( ) ) {
val groupRecord = getGroup ( cursor )
if ( groupRecord . isPresent && RemappedRecords . getInstance ( ) . areAnyRemapped ( groupRecord . get ( ) . members ) ) {
2023-01-24 13:59:01 +00:00
val groupId = groupRecord . get ( ) . id
2023-01-02 04:05:00 +00:00
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 )
2023-01-24 13:59:01 +00:00
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 }
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 ( )
}
}
}
2023-01-02 04:05:00 +00:00
if ( updateCount > 0 ) {
2023-02-16 18:03:10 +00:00
Log . i ( TAG , " Successfully updated $updateCount rows. GroupId: $groupId , Remaps: $remaps " , true )
2023-01-02 04:05:00 +00:00
} else {
2023-02-16 18:03:10 +00:00
Log . w ( TAG , " Failed to update any rows. GroupId: $groupId , Remaps: $remaps " , true )
2023-01-02 04:05:00 +00:00
}
}
2023-02-16 18:03:10 +00:00
getGroup ( cursor )
2023-01-02 04:05:00 +00:00
} else {
Optional . empty ( )
}
}
}
/ * *
* Call if you are sure this group should exist .
* Finds group and throws if it cannot .
* /
fun requireGroup ( groupId : GroupId ) : GroupRecord {
val group = getGroup ( groupId )
if ( ! group . isPresent ) {
throw AssertionError ( " Group not found " )
}
return group . get ( )
}
fun groupExists ( groupId : GroupId ) : Boolean {
return readableDatabase
. exists ( TABLE _NAME )
. where ( " $GROUP _ID = ? " , groupId . toString ( ) )
. run ( )
}
/ * *
* @return A gv1 group whose expected v2 ID matches the one provided .
* /
fun getGroupV1ByExpectedV2 ( gv2Id : GroupId . V2 ) : Optional < GroupRecord > {
2023-01-24 13:59:01 +00:00
return getGroup ( SqlUtil . Query ( " $TABLE _NAME. $EXPECTED _V2_ID = ? " , buildArgs ( gv2Id ) ) )
2023-01-02 04:05:00 +00:00
}
fun getGroupByDistributionId ( distributionId : DistributionId ) : Optional < GroupRecord > {
2023-01-24 13:59:01 +00:00
return getGroup ( SqlUtil . Query ( " $TABLE _NAME. $DISTRIBUTION _ID = ? " , buildArgs ( distributionId ) ) )
2023-01-02 04:05:00 +00:00
}
fun removeUnmigratedV1Members ( id : GroupId . V2 ) {
val group = getGroup ( id )
if ( ! group . isPresent ) {
Log . w ( TAG , " Couldn't find the group! " , Throwable ( ) )
return
}
removeUnmigratedV1Members ( id , group . get ( ) . unmigratedV1Members )
}
/ * *
* Removes the specified members from the list of ' unmigrated V1 members ' -- the list of members
* that were either dropped or had to be invited when migrating the group from V1 -> V2 .
* /
fun removeUnmigratedV1Members ( id : GroupId . V2 , toRemove : List < RecipientId > ) {
val group = getGroup ( id )
if ( group . isAbsent ( ) ) {
Log . w ( TAG , " Couldn't find the group! " , Throwable ( ) )
return
}
val newUnmigrated = group . get ( ) . unmigratedV1Members - toRemove . toSet ( )
writableDatabase
. update ( TABLE _NAME )
. values ( UNMIGRATED _V1 _MEMBERS to if ( newUnmigrated . isEmpty ( ) ) null else newUnmigrated . serialize ( ) )
. where ( " $GROUP _ID = ? " , id )
. run ( )
Recipient . live ( Recipient . externalGroupExact ( id ) . id ) . refresh ( )
}
private fun getGroup ( cursor : Cursor ? ) : Optional < GroupRecord > {
val reader = Reader ( cursor )
return Optional . ofNullable ( reader . getCurrent ( ) )
}
/ * *
* @return local db group revision or - 1 if not present .
* /
fun getGroupV2Revision ( groupId : GroupId . V2 ) : Int {
readableDatabase
. select ( )
. from ( TABLE _NAME )
. where ( " $GROUP _ID = ? " , groupId . toString ( ) )
. run ( )
. use { cursor ->
return if ( cursor . moveToNext ( ) ) {
cursor . getInt ( cursor . getColumnIndexOrThrow ( V2 _REVISION ) )
} else {
- 1
}
}
}
fun isUnknownGroup ( groupId : GroupId ) : Boolean {
val group = getGroup ( groupId )
if ( ! group . isPresent ) {
return true
}
val noMetadata = ! group . get ( ) . hasAvatar ( ) && group . get ( ) . title . isNullOrEmpty ( )
val noMembers = group . get ( ) . members . isEmpty ( ) || group . get ( ) . members . size == 1 && group . get ( ) . members . contains ( Recipient . self ( ) . id )
return noMetadata && noMembers
}
2023-01-26 18:10:38 +00:00
fun queryGroupsByMemberName ( inputQuery : String ) : Cursor {
val subquery = recipients . getAllContactsSubquery ( inputQuery )
val statement = """
SELECT
DISTINCT $ TABLE_NAME . * ,
GROUP _CONCAT ( $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } ) as $ MEMBER _GROUP _CONCAT ,
$ { ThreadTable . TABLE _NAME } . $ { ThreadTable . DATE } as $ THREAD _DATE
FROM $ TABLE _NAME
INNER JOIN $ { MembershipTable . TABLE _NAME } ON $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
INNER JOIN $ { ThreadTable . TABLE _NAME } ON $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . RECIPIENT _ID } = $ TABLE_NAME . $ RECIPIENT _ID
WHERE $ ACTIVE = 1 AND $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } IN ( $ { subquery . where } )
GROUP BY $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID }
ORDER BY $ TITLE COLLATE NOCASE ASC
""" .toSingleLine()
return databaseHelper . signalReadableDatabase . query ( statement , subquery . whereArgs )
}
2023-01-02 04:05:00 +00:00
fun queryGroupsByTitle ( inputQuery : String , includeInactive : Boolean , excludeV1 : Boolean , excludeMms : Boolean ) : Reader {
val query = getGroupQueryWhereStatement ( inputQuery , includeInactive , excludeV1 , excludeMms )
2023-01-25 20:51:35 +00:00
//language=sql
2023-01-24 13:59:01 +00:00
val statement = """
$ JOINED _GROUP _SELECT
WHERE $ { query . where }
ORDER BY $ TITLE COLLATE NOCASE ASC
""" .trimIndent()
val cursor = databaseHelper . signalReadableDatabase . query ( statement , query . whereArgs )
2023-01-02 04:05:00 +00:00
return Reader ( cursor )
}
fun queryGroupsByMembership ( recipientIds : Set < RecipientId > , includeInactive : Boolean , excludeV1 : Boolean , excludeMms : Boolean ) : Reader {
var recipientIds = recipientIds
if ( recipientIds . isEmpty ( ) ) {
return Reader ( null )
}
if ( recipientIds . size > 30 ) {
Log . w ( TAG , " [queryGroupsByMembership] Large set of recipientIds ( ${recipientIds.size} )! Using the first 30. " )
recipientIds = recipientIds . take ( 30 ) . toSet ( )
}
2023-01-24 13:59:01 +00:00
val membershipQuery = SqlUtil . buildSingleCollectionQuery ( " ${MembershipTable.TABLE_NAME} . ${MembershipTable.RECIPIENT_ID} " , recipientIds )
2023-01-02 04:05:00 +00:00
var query : String
val queryArgs : Array < String >
if ( includeInactive ) {
2023-01-24 13:59:01 +00:00
query = " ${membershipQuery.where} AND ( $ACTIVE = ? OR $TABLE _NAME. $RECIPIENT _ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} )) "
queryArgs = membershipQuery . whereArgs + buildArgs ( 1 )
2023-01-02 04:05:00 +00:00
} else {
2023-01-24 13:59:01 +00:00
query = " ${membershipQuery.where} AND $ACTIVE = ? "
queryArgs = membershipQuery . whereArgs + buildArgs ( 1 )
2023-01-02 04:05:00 +00:00
}
if ( excludeV1 ) {
query += " AND $EXPECTED _V2_ID IS NULL "
}
if ( excludeMms ) {
query += " AND $MMS = 0 "
}
2023-01-27 18:33:03 +00:00
val selection = """
SELECT DISTINCT
$ TABLE_NAME . * ,
(
SELECT GROUP _CONCAT ( $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } )
FROM $ { MembershipTable . TABLE _NAME }
WHERE $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
) as $ MEMBER _GROUP _CONCAT
FROM $ { MembershipTable . TABLE _NAME }
INNER JOIN $ TABLE _NAME ON $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
WHERE $ query
""" .trimIndent()
return Reader ( readableDatabase . query ( selection , queryArgs ) )
2023-01-02 04:05:00 +00:00
}
private fun queryGroupsByRecency ( groupQuery : GroupQuery ) : Reader {
val query = getGroupQueryWhereStatement ( groupQuery . searchQuery , groupQuery . includeInactive , ! groupQuery . includeV1 , ! groupQuery . includeMms )
val sql = """
2023-01-24 13:59:01 +00:00
$ JOINED _GROUP _SELECT
2023-01-26 14:59:02 +00:00
INNER JOIN $ { ThreadTable . TABLE _NAME } ON $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . RECIPIENT _ID } = $ TABLE_NAME . $ RECIPIENT _ID
2023-01-02 04:05:00 +00:00
WHERE $ { query . where }
ORDER BY $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . DATE } DESC
""" .toSingleLine()
return Reader ( databaseHelper . signalReadableDatabase . rawQuery ( sql , query . whereArgs ) )
}
fun queryGroups ( groupQuery : GroupQuery ) : Reader {
return if ( groupQuery . sortOrder === ContactSearchSortOrder . NATURAL ) {
queryGroupsByTitle ( groupQuery . searchQuery , groupQuery . includeInactive , ! groupQuery . includeV1 , ! groupQuery . includeMms )
} else {
queryGroupsByRecency ( groupQuery )
}
}
private fun getGroupQueryWhereStatement ( inputQuery : String , includeInactive : Boolean , excludeV1 : Boolean , excludeMms : Boolean ) : SqlUtil . Query {
var query : String
val queryArgs : Array < String >
val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern ( inputQuery )
if ( includeInactive ) {
2023-01-24 13:59:01 +00:00
query = " $TITLE GLOB ? AND ( $ACTIVE = ? OR $TABLE _NAME. $RECIPIENT _ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} )) "
2023-01-02 04:05:00 +00:00
queryArgs = buildArgs ( caseInsensitiveQuery , 1 )
} else {
query = " $TITLE GLOB ? AND $ACTIVE = ? "
queryArgs = buildArgs ( caseInsensitiveQuery , 1 )
}
if ( excludeV1 ) {
query += " AND $EXPECTED _V2_ID IS NULL "
}
if ( excludeMms ) {
query += " AND $MMS = 0 "
}
return SqlUtil . Query ( query , queryArgs )
}
fun getOrCreateDistributionId ( groupId : GroupId . V2 ) : DistributionId {
readableDatabase
. select ( DISTRIBUTION _ID )
. from ( TABLE _NAME )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
. use { cursor ->
return if ( cursor . moveToFirst ( ) ) {
val serialized = cursor . optionalString ( DISTRIBUTION _ID )
if ( serialized . isPresent ) {
DistributionId . from ( serialized . get ( ) )
} else {
Log . w ( TAG , " Missing distributionId! Creating one. " )
val distributionId = DistributionId . create ( )
val count = writableDatabase
. update ( TABLE _NAME )
. values ( DISTRIBUTION _ID to distributionId . toString ( ) )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
check ( count >= 1 ) { " Tried to create a distributionId for $groupId , but it doesn't exist! " }
distributionId
}
} else {
throw IllegalStateException ( " Group $groupId doesn't exist! " )
}
}
}
2023-01-24 13:59:01 +00:00
fun getOrCreateMmsGroupForMembers ( members : Set < RecipientId > ) : GroupId . Mms {
2023-02-09 02:12:00 +00:00
val joinedTestMembers = members
. toList ( )
. map { it . toLong ( ) }
. sorted ( )
. joinToString ( separator = " , " )
2023-01-24 13:59:01 +00:00
//language=sql
val statement = """
2023-02-09 02:12:00 +00:00
SELECT
$ TABLE_NAME . $ GROUP _ID as gid ,
(
SELECT GROUP _CONCAT ( $ { MembershipTable . RECIPIENT _ID } , ',' )
FROM (
SELECT $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID }
FROM $ { MembershipTable . TABLE _NAME }
WHERE $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
ORDER BY $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } ASC
)
) as $ MEMBER _GROUP _CONCAT
FROM $ TABLE _NAME
WHERE $ MEMBER _GROUP _CONCAT = ?
2023-01-24 13:59:01 +00:00
""" .toSingleLine()
2023-01-02 04:05:00 +00:00
2023-02-09 02:12:00 +00:00
return readableDatabase . rawQuery ( statement , buildArgs ( joinedTestMembers ) ) . use { cursor ->
2023-01-24 13:59:01 +00:00
if ( cursor . moveToNext ( ) ) {
return GroupId . parseOrThrow ( cursor . requireNonNullString ( " gid " ) ) . requireMms ( )
} else {
val groupId = GroupId . createMms ( SecureRandom ( ) )
create ( groupId , null , members )
groupId
2023-01-02 04:05:00 +00:00
}
2023-01-24 13:59:01 +00:00
}
2023-01-02 04:05:00 +00:00
}
@WorkerThread
fun getPushGroupNamesContainingMember ( recipientId : RecipientId ) : List < String > {
return getPushGroupsContainingMember ( recipientId )
. map { groupRecord -> Recipient . resolved ( groupRecord . recipientId ) . getDisplayName ( context ) }
. toList ( )
}
@WorkerThread
fun getPushGroupsContainingMember ( recipientId : RecipientId ) : List < GroupRecord > {
return getGroupsContainingMember ( recipientId , true )
}
fun getGroupsContainingMember ( recipientId : RecipientId , pushOnly : Boolean ) : List < GroupRecord > {
return getGroupsContainingMember ( recipientId , pushOnly , false )
}
@WorkerThread
fun getGroupsContainingMember ( recipientId : RecipientId , pushOnly : Boolean , includeInactive : Boolean ) : List < GroupRecord > {
2023-01-24 13:59:01 +00:00
//language=sql
val table = """
2023-01-27 18:33:03 +00:00
SELECT
DISTINCT $ TABLE_NAME . * ,
(
SELECT GROUP _CONCAT ( $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . RECIPIENT _ID } )
FROM $ { MembershipTable . TABLE _NAME }
WHERE $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
) as $ MEMBER _GROUP _CONCAT
FROM $ { MembershipTable . TABLE _NAME }
INNER JOIN $ TABLE _NAME ON $ { MembershipTable . TABLE _NAME } . $ { MembershipTable . GROUP _ID } = $ TABLE_NAME . $ GROUP _ID
2023-02-15 18:11:24 +00:00
LEFT JOIN $ { ThreadTable . TABLE _NAME } ON $ TABLE_NAME . $ RECIPIENT _ID = $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . RECIPIENT _ID }
2023-01-24 13:59:01 +00:00
""" .toSingleLine()
var query = " ${MembershipTable.TABLE_NAME} . ${MembershipTable.RECIPIENT_ID} = ? "
var args = buildArgs ( recipientId )
2023-01-02 04:05:00 +00:00
val orderBy = " ${ThreadTable.TABLE_NAME} . ${ThreadTable.DATE} DESC "
if ( pushOnly ) {
query += " AND $MMS = ? "
args = appendArg ( args , " 0 " )
}
if ( !in cludeInactive ) {
query += " AND $ACTIVE = ? "
args = appendArg ( args , " 1 " )
}
return readableDatabase
2023-01-27 18:33:03 +00:00
. query ( " $table WHERE $query ORDER BY $orderBy " , args )
2023-01-02 04:05:00 +00:00
. readToList { cursor ->
2023-01-24 13:59:01 +00:00
getGroup ( cursor ) . get ( )
2023-01-02 04:05:00 +00:00
}
}
fun getGroups ( ) : Reader {
2023-01-27 18:33:03 +00:00
val cursor = readableDatabase . query ( JOINED _GROUP _SELECT )
2023-01-02 04:05:00 +00:00
return Reader ( cursor )
}
fun getActiveGroupCount ( ) : Int {
return readableDatabase
. select ( " COUNT(*) " )
. from ( TABLE _NAME )
. where ( " $ACTIVE = ? " , 1 )
. run ( )
. readToSingleInt ( 0 )
}
@WorkerThread
fun getGroupMemberIds ( groupId : GroupId , memberSet : MemberSet ) : List < RecipientId > {
return if ( groupId . isV2 ) {
getGroup ( groupId )
. map { it . requireV2GroupProperties ( ) . getMemberRecipientIds ( memberSet ) }
. orElse ( emptyList ( ) )
} else {
val currentMembers : MutableList < RecipientId > = getCurrentMembers ( groupId )
if ( ! memberSet . includeSelf ) {
currentMembers -= Recipient . self ( ) . id
}
currentMembers
}
}
@WorkerThread
fun getGroupMembers ( groupId : GroupId , memberSet : MemberSet ) : List < Recipient > {
return if ( groupId . isV2 ) {
getGroup ( groupId )
. map { it . requireV2GroupProperties ( ) . getMemberRecipients ( memberSet ) }
. orElse ( emptyList ( ) )
} else {
val currentMembers : List < RecipientId > = getCurrentMembers ( groupId )
val recipients : MutableList < Recipient > = ArrayList ( currentMembers . size )
for ( member in currentMembers ) {
val resolved = Recipient . resolved ( member )
if ( memberSet . includeSelf || ! resolved . isSelf ) {
recipients += resolved
}
}
recipients
}
}
fun create ( groupId : GroupId . V1 , title : String ? , members : Collection < RecipientId > , avatar : SignalServiceAttachmentPointer ? , relay : String ? ) {
if ( groupExists ( groupId . deriveV2MigrationGroupId ( ) ) ) {
throw LegacyGroupInsertException ( groupId )
}
create ( groupId , title , members , avatar , relay , null , null )
}
fun create ( groupId : GroupId . Mms , title : String ? , members : Collection < RecipientId > ) {
create ( groupId , if ( title . isNullOrEmpty ( ) ) null else title , members , null , null , null , null )
}
@JvmOverloads
fun create ( groupMasterKey : GroupMasterKey , groupState : DecryptedGroup , force : Boolean = false ) : GroupId . V2 {
val groupId = GroupId . v2 ( groupMasterKey )
if ( ! force && getGroupV1ByExpectedV2 ( groupId ) . isPresent ) {
throw MissedGroupMigrationInsertException ( groupId )
} else if ( force ) {
Log . w ( TAG , " Forcing the creation of a group even though we already have a V1 ID! " )
}
create ( groupId , groupState . title , emptyList ( ) , null , null , groupMasterKey , groupState )
return groupId
}
/ * *
* There was a point in time where we weren ' t properly responding to group creates on linked devices . This would result in us having a Recipient entry for the
* group , but we ' d either be missing the group entry , or that entry would be missing a master key . This method fixes this scenario .
* /
2023-02-26 18:45:24 +00:00
fun fixMissingMasterKey ( groupMasterKey : GroupMasterKey ) {
2023-01-02 04:05:00 +00:00
val groupId = GroupId . v2 ( groupMasterKey )
if ( getGroupV1ByExpectedV2 ( groupId ) . isPresent ) {
Log . w ( TAG , " There already exists a V1 group that should be migrated into this group. But if the recipient already exists, there's not much we can do here. " )
}
writableDatabase . withinTransaction { db ->
val updated = db
. update ( TABLE _NAME )
. values ( V2 _MASTER _KEY to groupMasterKey . serialize ( ) )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
if ( updated < 1 ) {
Log . w ( TAG , " No group entry. Creating restore placeholder for $groupId " )
create (
groupMasterKey ,
DecryptedGroup . newBuilder ( )
. setRevision ( GroupsV2StateProcessor . RESTORE _PLACEHOLDER _REVISION )
. build ( ) ,
true
)
} else {
Log . w ( TAG , " Had a group entry, but it was missing a master key. Updated. " )
}
}
Log . w ( TAG , " Scheduling request for latest group info for $groupId " )
ApplicationDependencies . getJobManager ( ) . add ( RequestGroupV2InfoJob ( groupId ) )
}
/ * *
* @param groupMasterKey null for V1 , must be non - null for V2 ( presence dictates group version ) .
* /
private fun create (
groupId : GroupId ,
title : String ? ,
memberCollection : Collection < RecipientId > ,
avatar : SignalServiceAttachmentPointer ? ,
relay : String ? ,
groupMasterKey : GroupMasterKey ? ,
groupState : DecryptedGroup ?
) {
2023-01-24 13:59:01 +00:00
val membershipValues = mutableListOf < ContentValues > ( )
2023-01-02 04:05:00 +00:00
val groupRecipientId = recipients . getOrInsertFromGroupId ( groupId )
val members : List < RecipientId > = memberCollection . toSet ( ) . sorted ( )
var groupMembers : List < RecipientId > = members
val values = ContentValues ( )
values . put ( RECIPIENT _ID , groupRecipientId . serialize ( ) )
values . put ( GROUP _ID , groupId . toString ( ) )
values . put ( TITLE , title )
2023-01-24 13:59:01 +00:00
membershipValues . addAll ( members . toContentValues ( groupId ) )
2023-01-02 04:05:00 +00:00
values . put ( MMS , groupId . isMms )
if ( avatar != null ) {
values . put ( AVATAR _ID , avatar . remoteId . v2 . get ( ) )
values . put ( AVATAR _KEY , avatar . key )
values . put ( AVATAR _CONTENT _TYPE , avatar . contentType )
values . put ( AVATAR _DIGEST , avatar . digest . orElse ( null ) )
} else {
values . put ( AVATAR _ID , 0 )
}
values . put ( AVATAR _RELAY , relay )
values . put ( TIMESTAMP , System . currentTimeMillis ( ) )
if ( groupId . isV2 ) {
values . put ( ACTIVE , if ( groupState != null && gv2GroupActive ( groupState ) ) 1 else 0 )
values . put ( DISTRIBUTION _ID , DistributionId . create ( ) . toString ( ) )
} else if ( groupId . isV1 ) {
values . put ( ACTIVE , 1 )
values . put ( EXPECTED _V2 _ID , groupId . requireV1 ( ) . deriveV2MigrationGroupId ( ) . toString ( ) )
} else {
values . put ( ACTIVE , 1 )
}
if ( groupMasterKey != null ) {
if ( groupState == null ) {
throw AssertionError ( " V2 master key but no group state " )
}
groupId . requireV2 ( )
groupMembers = getV2GroupMembers ( groupState , true )
values . put ( V2 _MASTER _KEY , groupMasterKey . serialize ( ) )
values . put ( V2 _REVISION , groupState . revision )
values . put ( V2 _DECRYPTED _GROUP , groupState . toByteArray ( ) )
2023-01-24 13:59:01 +00:00
membershipValues . clear ( )
membershipValues . addAll ( groupMembers . toContentValues ( groupId ) )
2023-01-02 04:05:00 +00:00
} else {
if ( groupId . isV2 ) {
throw AssertionError ( " V2 group id but no master key " )
}
}
2023-01-24 13:59:01 +00:00
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 )
}
}
2023-01-02 04:05:00 +00:00
if ( groupState != null && groupState . hasDisappearingMessagesTimer ( ) ) {
recipients . setExpireMessages ( groupRecipientId , groupState . disappearingMessagesTimer . duration )
}
if ( groupId . isMms || Recipient . resolved ( groupRecipientId ) . isProfileSharing ) {
recipients . setHasGroupsInCommon ( groupMembers )
}
Recipient . live ( groupRecipientId ) . refresh ( )
notifyConversationListListeners ( )
}
fun update ( groupId : GroupId . V1 , title : String ? , avatar : SignalServiceAttachmentPointer ? ) {
val contentValues = ContentValues ( ) . apply {
if ( title != null ) {
put ( TITLE , title )
}
if ( avatar != null ) {
put ( AVATAR _ID , avatar . remoteId . v2 . get ( ) )
put ( AVATAR _CONTENT _TYPE , avatar . contentType )
put ( AVATAR _KEY , avatar . key )
put ( AVATAR _DIGEST , avatar . digest . orElse ( null ) )
} else {
put ( AVATAR _ID , 0 )
}
}
writableDatabase
. update ( TABLE _NAME )
. values ( contentValues )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
val groupRecipient = recipients . getOrInsertFromGroupId ( groupId )
Recipient . live ( groupRecipient ) . refresh ( )
notifyConversationListListeners ( )
}
/ * *
* Migrates a V1 group to a V2 group .
*
* @param decryptedGroup The state that represents the group on the server . This will be used to
* determine if we need to save our old membership list and stuff .
* /
fun migrateToV2 (
threadId : Long ,
groupIdV1 : GroupId . V1 ,
decryptedGroup : DecryptedGroup
) : GroupId . V2 {
val groupIdV2 = groupIdV1 . deriveV2MigrationGroupId ( )
val groupMasterKey = groupIdV1 . deriveV2MigrationMasterKey ( )
writableDatabase . withinTransaction { db ->
val record = getGroup ( groupIdV1 ) . get ( )
val newMembers : MutableList < RecipientId > = DecryptedGroupUtil . membersToUuidList ( decryptedGroup . membersList ) . toRecipientIds ( )
val pendingMembers : List < RecipientId > = DecryptedGroupUtil . pendingToUuidList ( decryptedGroup . pendingMembersList ) . toRecipientIds ( )
newMembers . addAll ( pendingMembers )
val droppedMembers : List < RecipientId > = SetUtil . difference ( record . members , newMembers ) . toList ( )
val unmigratedMembers : List < RecipientId > = pendingMembers + droppedMembers
val updated : Int = db . update ( TABLE _NAME )
. values (
GROUP _ID to groupIdV2 . toString ( ) ,
V2 _MASTER _KEY to groupMasterKey . serialize ( ) ,
DISTRIBUTION _ID to DistributionId . create ( ) . toString ( ) ,
EXPECTED _V2 _ID to null ,
UNMIGRATED _V1 _MEMBERS to if ( unmigratedMembers . isEmpty ( ) ) null else unmigratedMembers . serialize ( )
)
. where ( " $GROUP _ID = ? " , groupIdV1 )
. run ( )
if ( updated != 1 ) {
throw AssertionError ( )
}
recipients . updateGroupId ( groupIdV1 , groupIdV2 )
update ( groupMasterKey , decryptedGroup )
messages . insertGroupV1MigrationEvents (
record . recipientId ,
threadId ,
GroupMigrationMembershipChange ( pendingMembers , droppedMembers )
)
}
return groupIdV2
}
fun update ( groupMasterKey : GroupMasterKey , decryptedGroup : DecryptedGroup ) {
update ( GroupId . v2 ( groupMasterKey ) , decryptedGroup )
}
fun update ( groupId : GroupId . V2 , decryptedGroup : DecryptedGroup ) {
val groupRecipientId : RecipientId = recipients . getOrInsertFromGroupId ( groupId )
val existingGroup : Optional < GroupRecord > = getGroup ( groupId )
val title : String = decryptedGroup . title
val contentValues = ContentValues ( )
contentValues . put ( TITLE , title )
contentValues . put ( V2 _REVISION , decryptedGroup . revision )
contentValues . put ( V2 _DECRYPTED _GROUP , decryptedGroup . toByteArray ( ) )
contentValues . put ( ACTIVE , if ( gv2GroupActive ( decryptedGroup ) ) 1 else 0 )
if ( existingGroup . isPresent && existingGroup . get ( ) . unmigratedV1Members . isNotEmpty ( ) && existingGroup . get ( ) . isV2Group ) {
val unmigratedV1Members : MutableSet < RecipientId > = existingGroup . get ( ) . unmigratedV1Members . toMutableSet ( )
val change = GroupChangeReconstruct . reconstructGroupChange ( existingGroup . get ( ) . requireV2GroupProperties ( ) . decryptedGroup , decryptedGroup )
val addedMembers : Set < RecipientId > = DecryptedGroupUtil . membersToUuidList ( change . newMembersList ) . toRecipientIds ( ) . toSet ( )
val removedMembers : Set < RecipientId > = DecryptedGroupUtil . removedMembersUuidList ( change ) . toRecipientIds ( ) . toSet ( )
val addedInvites : Set < RecipientId > = DecryptedGroupUtil . pendingToUuidList ( change . newPendingMembersList ) . toRecipientIds ( ) . toSet ( )
val removedInvites : Set < RecipientId > = DecryptedGroupUtil . removedPendingMembersUuidList ( change ) . toRecipientIds ( ) . toSet ( )
val acceptedInvites : Set < RecipientId > = DecryptedGroupUtil . membersToUuidList ( change . promotePendingMembersList ) . toRecipientIds ( ) . toSet ( )
unmigratedV1Members -= addedMembers
unmigratedV1Members -= removedMembers
unmigratedV1Members -= addedInvites
unmigratedV1Members -= removedInvites
unmigratedV1Members -= acceptedInvites
contentValues . put ( UNMIGRATED _V1 _MEMBERS , if ( unmigratedV1Members . isEmpty ( ) ) null else unmigratedV1Members . serialize ( ) )
}
val groupMembers = getV2GroupMembers ( decryptedGroup , true )
if ( existingGroup . isPresent && existingGroup . get ( ) . isV2Group ) {
val change = GroupChangeReconstruct . reconstructGroupChange ( existingGroup . get ( ) . requireV2GroupProperties ( ) . decryptedGroup , decryptedGroup )
val removed : List < UUID > = DecryptedGroupUtil . removedMembersUuidList ( change )
if ( removed . isNotEmpty ( ) ) {
val distributionId = existingGroup . get ( ) . distributionId !!
Log . i ( TAG , removed . size . toString ( ) + " members were removed from group " + groupId + " . Rotating the DistributionId " + distributionId )
SenderKeyUtil . rotateOurKey ( distributionId )
}
}
2023-01-24 13:59:01 +00:00
writableDatabase . withinTransaction { database ->
database
. update ( TABLE _NAME )
. values ( contentValues )
. where ( " $GROUP _ID = ? " , groupId . toString ( ) )
. run ( )
performMembershipUpdate ( database , groupId , groupMembers )
}
2023-01-02 04:05:00 +00:00
if ( decryptedGroup . hasDisappearingMessagesTimer ( ) ) {
recipients . setExpireMessages ( groupRecipientId , decryptedGroup . disappearingMessagesTimer . duration )
}
if ( groupId . isMms || Recipient . resolved ( groupRecipientId ) . isProfileSharing ) {
recipients . setHasGroupsInCommon ( groupMembers )
}
Recipient . live ( groupRecipientId ) . refresh ( )
notifyConversationListListeners ( )
}
fun updateTitle ( groupId : GroupId . V1 , title : String ? ) {
updateTitle ( groupId as GroupId , title )
}
fun updateTitle ( groupId : GroupId . Mms , title : String ? ) {
updateTitle ( groupId as GroupId , if ( title . isNullOrEmpty ( ) ) null else title )
}
private fun updateTitle ( groupId : GroupId , title : String ? ) {
if ( ! groupId . isV1 && ! groupId . isMms ) {
throw AssertionError ( )
}
writableDatabase
. update ( TABLE _NAME )
. values ( TITLE to title )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
val groupRecipient = recipients . getOrInsertFromGroupId ( groupId )
Recipient . live ( groupRecipient ) . refresh ( )
}
/ * *
* Used to bust the Glide cache when an avatar changes .
* /
fun onAvatarUpdated ( groupId : GroupId , hasAvatar : Boolean ) {
writableDatabase
. update ( TABLE _NAME )
. values ( AVATAR _ID to if ( hasAvatar ) Math . abs ( SecureRandom ( ) . nextLong ( ) ) else 0 )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
val groupRecipient = recipients . getOrInsertFromGroupId ( groupId )
Recipient . live ( groupRecipient ) . refresh ( )
}
fun updateMembers ( groupId : GroupId , members : List < RecipientId > ) {
2023-01-24 13:59:01 +00:00
writableDatabase . withinTransaction { database ->
database
. update ( TABLE _NAME )
. values ( ACTIVE to 1 )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
performMembershipUpdate ( database , groupId , members )
}
2023-01-02 04:05:00 +00:00
val groupRecipient = recipients . getOrInsertFromGroupId ( groupId )
Recipient . live ( groupRecipient ) . refresh ( )
}
fun remove ( groupId : GroupId , source : RecipientId ) {
writableDatabase
2023-01-24 13:59:01 +00:00
. delete ( MembershipTable . TABLE _NAME )
. where ( " ${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? " , groupId , source )
2023-01-02 04:05:00 +00:00
. run ( )
val groupRecipient = recipients . getOrInsertFromGroupId ( groupId )
Recipient . live ( groupRecipient ) . refresh ( )
}
private fun getCurrentMembers ( groupId : GroupId ) : MutableList < RecipientId > {
return readableDatabase
2023-01-24 13:59:01 +00:00
. select ( MembershipTable . RECIPIENT _ID )
. from ( MembershipTable . TABLE _NAME )
. where ( " ${MembershipTable.GROUP_ID} = ? " , groupId )
2023-01-02 04:05:00 +00:00
. run ( )
. readToList { cursor ->
2023-01-24 13:59:01 +00:00
RecipientId . from ( cursor . requireLong ( MembershipTable . RECIPIENT _ID ) )
2023-01-02 04:05:00 +00:00
}
. toMutableList ( )
}
2023-01-24 13:59:01 +00:00
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 )
}
}
2023-01-02 04:05:00 +00:00
fun isActive ( groupId : GroupId ) : Boolean {
val record = getGroup ( groupId )
return record . isPresent && record . get ( ) . isActive
}
fun setActive ( groupId : GroupId , active : Boolean ) {
writableDatabase
. update ( TABLE _NAME )
. values ( ACTIVE to if ( active ) 1 else 0 )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
}
fun setLastForceUpdateTimestamp ( groupId : GroupId , timestamp : Long ) {
writableDatabase
. update ( TABLE _NAME )
. values ( LAST _FORCE _UPDATE _TIMESTAMP to timestamp )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
}
@WorkerThread
fun isCurrentMember ( groupId : Push , recipientId : RecipientId ) : Boolean {
2023-01-24 13:59:01 +00:00
return readableDatabase
. exists ( MembershipTable . TABLE _NAME )
. where ( " ${MembershipTable.GROUP_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? " , groupId , recipientId )
2023-01-02 04:05:00 +00:00
. run ( )
}
fun getAllGroupV2Ids ( ) : List < GroupId . V2 > {
return readableDatabase
. select ( GROUP _ID )
. from ( TABLE _NAME )
. run ( )
. readToList { GroupId . parseOrThrow ( it . requireNonNullString ( GROUP _ID ) ) }
. filter { it . isV2 }
. map { it . requireV2 ( ) }
}
/ * *
* Key : The ' expected ' V2 ID ( i . e . what a V1 ID would map to when migrated )
* Value : The matching V1 ID
* /
fun getAllExpectedV2Ids ( ) : Map < GroupId . V2 , GroupId . V1 > {
return readableDatabase
. select ( GROUP _ID , EXPECTED _V2 _ID )
. from ( TABLE _NAME )
. where ( " $EXPECTED _V2_ID NOT NULL " )
. run ( )
. readToList { cursor ->
val groupId = GroupId . parseOrThrow ( cursor . requireNonNullString ( GROUP _ID ) ) . requireV1 ( )
val expectedId = GroupId . parseOrThrow ( cursor . requireNonNullString ( EXPECTED _V2 _ID ) ) . requireV2 ( )
expectedId to groupId
}
. toMap ( )
}
override fun remapRecipient ( fromId : RecipientId , toId : RecipientId ) {
2023-01-24 13:59:01 +00:00
writableDatabase
. update ( MembershipTable . TABLE _NAME )
. values ( RECIPIENT _ID to toId . serialize ( ) )
. where ( " ${MembershipTable.RECIPIENT_ID} = ? " , fromId )
. run ( )
2023-01-02 04:05:00 +00:00
2023-01-24 13:59:01 +00:00
for ( group in getGroupsContainingMember ( fromId , false , true ) ) {
2023-01-02 04:05:00 +00:00
if ( group . isV2Group ) {
removeUnmigratedV1Members ( group . id . requireV2 ( ) , listOf ( fromId ) )
}
}
}
class Reader ( val cursor : Cursor ? ) : Closeable , ContactSearchIterator < GroupRecord > {
fun getNext ( ) : GroupRecord ? {
return if ( cursor == null || ! cursor . moveToNext ( ) ) {
null
} else {
getCurrent ( )
}
}
override fun getCount ( ) : Int {
return cursor ?. count ?: 0
}
fun getCurrent ( ) : GroupRecord ? {
return if ( cursor == null || cursor . requireString ( GROUP _ID ) == null || cursor . requireLong ( RECIPIENT _ID ) == 0L ) {
null
} else {
GroupRecord (
id = GroupId . parseOrThrow ( cursor . requireNonNullString ( GROUP _ID ) ) ,
recipientId = RecipientId . from ( cursor . requireNonNullString ( RECIPIENT _ID ) ) ,
title = cursor . requireString ( TITLE ) ,
2023-01-24 13:59:01 +00:00
serializedMembers = cursor . requireString ( MEMBER _GROUP _CONCAT ) ,
2023-01-02 04:05:00 +00:00
serializedUnmigratedV1Members = cursor . requireString ( UNMIGRATED _V1 _MEMBERS ) ,
avatarId = cursor . requireLong ( AVATAR _ID ) ,
avatarKey = cursor . requireBlob ( AVATAR _KEY ) ,
avatarContentType = cursor . requireString ( AVATAR _CONTENT _TYPE ) ,
relay = cursor . requireString ( AVATAR _RELAY ) ,
isActive = cursor . requireBoolean ( ACTIVE ) ,
avatarDigest = cursor . requireBlob ( AVATAR _DIGEST ) ,
isMms = cursor . requireBoolean ( MMS ) ,
groupMasterKeyBytes = cursor . requireBlob ( V2 _MASTER _KEY ) ,
groupRevision = cursor . requireInt ( V2 _REVISION ) ,
decryptedGroupBytes = cursor . requireBlob ( V2 _DECRYPTED _GROUP ) ,
distributionId = cursor . optionalString ( DISTRIBUTION _ID ) . map { id -> DistributionId . from ( id ) } . orElse ( null ) ,
lastForceUpdateTimestamp = cursor . requireLong ( LAST _FORCE _UPDATE _TIMESTAMP )
)
}
}
override fun close ( ) {
cursor ?. close ( )
}
override fun moveToPosition ( n : Int ) {
cursor !! . moveToPosition ( n )
}
override fun hasNext ( ) : Boolean {
return cursor != null && ! cursor . isLast && ! cursor . isAfterLast
}
override fun next ( ) : GroupRecord {
return getNext ( ) !!
}
}
class V2GroupProperties ( val groupMasterKey : GroupMasterKey , val groupRevision : Int , val decryptedGroupBytes : ByteArray ) {
val decryptedGroup : DecryptedGroup by lazy {
DecryptedGroup . parseFrom ( decryptedGroupBytes )
}
val bannedMembers : Set < UUID > by lazy {
DecryptedGroupUtil . bannedMembersToUuidSet ( decryptedGroup . bannedMembersList )
}
fun isAdmin ( recipient : Recipient ) : Boolean {
val serviceId = recipient . serviceId
return if ( serviceId . isPresent ) {
DecryptedGroupUtil . findMemberByUuid ( decryptedGroup . membersList , serviceId . get ( ) . uuid ( ) )
. map { it . role == Member . Role . ADMINISTRATOR }
. orElse ( false )
} else {
false
}
}
fun getAdmins ( members : List < Recipient > ) : List < Recipient > {
return members . stream ( ) . filter { recipient : Recipient -> isAdmin ( recipient ) } . collect ( Collectors . toList ( ) )
}
fun memberLevel ( serviceId : Optional < ServiceId > ) : MemberLevel {
if ( ! serviceId . isPresent ) {
return MemberLevel . NOT _A _MEMBER
}
var memberLevel : Optional < MemberLevel > = DecryptedGroupUtil . findMemberByUuid ( decryptedGroup . membersList , serviceId . get ( ) . uuid ( ) )
. map { member ->
if ( member . role == Member . Role . ADMINISTRATOR ) {
MemberLevel . ADMINISTRATOR
} else {
MemberLevel . FULL _MEMBER
}
}
if ( memberLevel . isAbsent ( ) ) {
memberLevel = DecryptedGroupUtil . findPendingByUuid ( decryptedGroup . pendingMembersList , serviceId . get ( ) . uuid ( ) )
. map { MemberLevel . PENDING _MEMBER }
}
if ( memberLevel . isAbsent ( ) ) {
memberLevel = DecryptedGroupUtil . findRequestingByUuid ( decryptedGroup . requestingMembersList , serviceId . get ( ) . uuid ( ) )
2023-02-26 18:45:24 +00:00
. map { _ -> MemberLevel . REQUESTING _MEMBER }
2023-01-02 04:05:00 +00:00
}
return if ( memberLevel . isPresent ) {
memberLevel . get ( )
} else {
MemberLevel . NOT _A _MEMBER
}
}
fun getMemberRecipients ( memberSet : MemberSet ) : List < Recipient > {
return Recipient . resolvedList ( getMemberRecipientIds ( memberSet ) )
}
fun getMemberRecipientIds ( memberSet : MemberSet ) : List < RecipientId > {
val includeSelf = memberSet . includeSelf
val selfUuid = SignalStore . account ( ) . requireAci ( ) . uuid ( )
val recipients : MutableList < RecipientId > = ArrayList ( decryptedGroup . membersCount + decryptedGroup . pendingMembersCount )
var unknownMembers = 0
var unknownPending = 0
for ( uuid in DecryptedGroupUtil . toUuidList ( decryptedGroup . membersList ) ) {
if ( UuidUtil . UNKNOWN _UUID == uuid ) {
unknownMembers ++
} else if ( includeSelf || selfUuid != uuid ) {
recipients += RecipientId . from ( ServiceId . from ( uuid ) )
}
}
if ( memberSet . includePending ) {
for ( uuid in DecryptedGroupUtil . pendingToUuidList ( decryptedGroup . pendingMembersList ) ) {
if ( UuidUtil . UNKNOWN _UUID == uuid ) {
unknownPending ++
} else if ( includeSelf || selfUuid != uuid ) {
recipients += RecipientId . from ( ServiceId . from ( uuid ) )
}
}
}
if ( unknownMembers + unknownPending > 0 ) {
Log . w ( TAG , " Group contains $unknownPending unknown pending and $unknownMembers unknown full members " )
}
return recipients
}
}
@Throws ( BadGroupIdException :: class )
fun getGroupsToDisplayAsStories ( ) : List < GroupId > {
@Language ( " sql " )
val query = """
SELECT
$ GROUP _ID ,
(
SELECT $ { MessageTable . TABLE _NAME } . $ { MessageTable . DATE _RECEIVED }
FROM $ { MessageTable . TABLE _NAME }
WHERE
$ { MessageTable . TABLE _NAME } . $ { MessageTable . RECIPIENT _ID } = $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . RECIPIENT _ID } AND
$ { MessageTable . STORY _TYPE } > 1
ORDER BY $ { MessageTable . TABLE _NAME } . $ { MessageTable . DATE _RECEIVED } DESC
LIMIT 1
) AS active _timestamp
FROM $ TABLE _NAME INNER JOIN $ { ThreadTable . TABLE _NAME } ON $ { ThreadTable . TABLE _NAME } . $ { ThreadTable . RECIPIENT _ID } = $ TABLE_NAME . $ RECIPIENT _ID
WHERE
$ ACTIVE = 1 AND
(
$ SHOW _AS _STORY _STATE = $ { ShowAsStoryState . ALWAYS . code } OR
(
$ SHOW _AS _STORY _STATE = $ { ShowAsStoryState . IF_ACTIVE . code } AND
active _timestamp IS NOT NULL
)
)
ORDER BY active _timestamp DESC
""" .toSingleLine()
return readableDatabase
. query ( query )
. readToList { cursor ->
GroupId . parse ( cursor . requireNonNullString ( GROUP _ID ) )
}
}
fun getShowAsStoryState ( groupId : GroupId ) : ShowAsStoryState {
return readableDatabase
. select ( SHOW _AS _STORY _STATE )
. from ( TABLE _NAME )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
. readToSingleObject { cursor ->
val serializedState = cursor . requireInt ( SHOW _AS _STORY _STATE )
ShowAsStoryState . deserialize ( serializedState )
} ?: throw AssertionError ( " Group $groupId does not exist! " )
}
fun setShowAsStoryState ( groupId : GroupId , showAsStoryState : ShowAsStoryState ) {
writableDatabase
. update ( TABLE _NAME )
. values ( SHOW _AS _STORY _STATE to showAsStoryState . code )
. where ( " $GROUP _ID = ? " , groupId )
. run ( )
}
fun setShowAsStoryState ( recipientIds : Collection < RecipientId ? > , showAsStoryState : ShowAsStoryState ) {
val queries = buildCollectionQuery ( RECIPIENT _ID , recipientIds )
val contentValues = contentValuesOf ( SHOW _AS _STORY _STATE to showAsStoryState . code )
writableDatabase . withinTransaction { db ->
for ( query in queries ) {
db . update ( TABLE _NAME , contentValues , query . where , query . whereArgs )
}
}
}
private fun gv2GroupActive ( decryptedGroup : DecryptedGroup ) : Boolean {
val aci = SignalStore . account ( ) . requireAci ( )
return DecryptedGroupUtil . findMemberByUuid ( decryptedGroup . membersList , aci . uuid ( ) ) . isPresent ||
DecryptedGroupUtil . findPendingByUuid ( decryptedGroup . pendingMembersList , aci . uuid ( ) ) . isPresent
}
private fun List < UUID > . toRecipientIds ( ) : MutableList < RecipientId > {
return uuidsToRecipientIds ( this )
}
private fun Collection < RecipientId > . serialize ( ) : String {
return RecipientId . toSerializedList ( this )
}
2023-01-24 13:59:01 +00:00
private fun Collection < RecipientId > . toContentValues ( groupId : GroupId ) : List < ContentValues > {
return map {
contentValuesOf (
MembershipTable . GROUP _ID to groupId . serialize ( ) ,
MembershipTable . RECIPIENT _ID to it . serialize ( )
)
}
}
2023-01-02 04:05:00 +00:00
private fun uuidsToRecipientIds ( uuids : List < UUID > ) : MutableList < RecipientId > {
return uuids
. asSequence ( )
. map { uuid ->
if ( uuid == UuidUtil . UNKNOWN _UUID ) {
Log . w ( TAG , " Saw an unknown UUID when mapping to RecipientIds! " )
null
} else {
val id = RecipientId . from ( ServiceId . from ( uuid ) )
val remapped = RemappedRecords . getInstance ( ) . getRecipient ( id )
if ( remapped . isPresent ) {
Log . w ( TAG , " Saw that $id remapped to $remapped . Using the mapping. " )
remapped . get ( )
} else {
id
}
}
}
. filterNotNull ( )
. sorted ( )
. toMutableList ( )
}
private fun getV2GroupMembers ( decryptedGroup : DecryptedGroup , shouldRetry : Boolean ) : List < RecipientId > {
val uuids : List < UUID > = DecryptedGroupUtil . membersToUuidList ( decryptedGroup . membersList )
val ids : List < RecipientId > = uuidsToRecipientIds ( uuids )
return if ( RemappedRecords . getInstance ( ) . areAnyRemapped ( ids ) ) {
if ( shouldRetry ) {
Log . w ( TAG , " Found remapped records where we shouldn't. Clearing cache and trying again. " )
RecipientId . clearCache ( )
RemappedRecords . getInstance ( ) . resetCache ( )
getV2GroupMembers ( decryptedGroup , false )
} else {
throw IllegalStateException ( " Remapped records in group membership! " )
}
} else {
ids
}
}
enum class MemberSet ( val includeSelf : Boolean , val includePending : Boolean ) {
FULL _MEMBERS _INCLUDING _SELF ( true , false ) , FULL _MEMBERS _EXCLUDING _SELF ( false , false ) , FULL _MEMBERS _AND _PENDING _INCLUDING _SELF ( true , true ) , FULL _MEMBERS _AND _PENDING _EXCLUDING _SELF ( false , true ) ;
}
/ * *
* State object describing whether or not to display a story in a list .
* /
enum class ShowAsStoryState ( val code : Int ) {
/ * *
* The default value . Display the group as a story if the group has stories in it currently .
* /
IF _ACTIVE ( 0 ) ,
/ * *
* Always display the group as a story unless explicitly removed . This state is entered if the
* user sends a story to a group or otherwise explicitly selects it to appear .
* /
ALWAYS ( 1 ) ,
/ * *
* Never display the story as a group . This state is entered if the user removes the group from
* their list , and is only navigated away from if the user explicitly adds the group again .
* /
NEVER ( 2 ) ;
companion object {
fun deserialize ( code : Int ) : ShowAsStoryState {
return when ( code ) {
0 -> IF _ACTIVE
1 -> ALWAYS
2 -> NEVER
else -> throw IllegalArgumentException ( " Unknown code: $code " )
}
}
}
}
enum class MemberLevel ( val isInGroup : Boolean ) {
NOT _A _MEMBER ( false ) ,
PENDING _MEMBER ( false ) ,
REQUESTING _MEMBER ( false ) ,
FULL _MEMBER ( true ) ,
ADMINISTRATOR ( true )
}
class GroupQuery private constructor ( builder : Builder ) {
val searchQuery : String
val includeInactive : Boolean
val includeV1 : Boolean
val includeMms : Boolean
val sortOrder : ContactSearchSortOrder
init {
searchQuery = builder . searchQuery
includeInactive = builder . includeInactive
includeV1 = builder . includeV1
includeMms = builder . includeMms
sortOrder = builder . sortOrder
}
class Builder {
var searchQuery = " "
var includeInactive = false
var includeV1 = false
var includeMms = false
var sortOrder = ContactSearchSortOrder . NATURAL
fun withSearchQuery ( query : String ? ) : Builder {
searchQuery = if ( TextUtils . isEmpty ( query ) ) " " else query !!
return this
}
fun withInactiveGroups ( includeInactive : Boolean ) : Builder {
this . includeInactive = includeInactive
return this
}
fun withV1Groups ( includeV1Groups : Boolean ) : Builder {
includeV1 = includeV1Groups
return this
}
fun withMmsGroups ( includeMmsGroups : Boolean ) : Builder {
includeMms = includeMmsGroups
return this
}
fun withSortOrder ( sortOrder : ContactSearchSortOrder ) : Builder {
this . sortOrder = sortOrder
return this
}
fun build ( ) : GroupQuery {
return GroupQuery ( this )
}
}
}
class LegacyGroupInsertException ( id : GroupId ? ) : IllegalStateException ( " Tried to create a new GV1 entry when we already had a migrated GV2! $id " )
class MissedGroupMigrationInsertException ( id : GroupId ? ) : IllegalStateException ( " Tried to create a new GV2 entry when we already had a V1 group that mapped to the new ID! $id " )
}