2022-02-24 17:40:28 +00:00
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
2022-03-23 16:56:11 +00:00
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
2022-06-24 14:51:26 +00:00
import org.signal.core.util.delete
2022-03-25 17:27:03 +00:00
import org.signal.core.util.logging.Log
2022-06-24 14:51:26 +00:00
import org.signal.core.util.readToList
2022-03-23 16:56:11 +00:00
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
2022-06-24 14:51:26 +00:00
import org.signal.core.util.requireObject
2022-03-23 16:56:11 +00:00
import org.signal.core.util.requireString
2022-05-10 14:01:51 +00:00
import org.signal.core.util.select
2022-06-24 14:51:26 +00:00
import org.signal.core.util.withinTransaction
2022-02-24 17:40:28 +00:00
import org.thoughtcrime.securesms.database.model.DistributionListId
2022-06-24 14:51:26 +00:00
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
2022-02-24 17:40:28 +00:00
import org.thoughtcrime.securesms.database.model.DistributionListRecord
2022-03-01 16:41:45 +00:00
import org.thoughtcrime.securesms.database.model.StoryType
2022-02-24 17:40:28 +00:00
import org.thoughtcrime.securesms.recipients.RecipientId
2022-03-25 17:27:03 +00:00
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
2022-02-24 17:40:28 +00:00
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Base64
import org.whispersystems.signalservice.api.push.DistributionId
2022-03-25 17:27:03 +00:00
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
import org.whispersystems.signalservice.api.util.UuidUtil
2022-02-24 17:40:28 +00:00
import java.util.UUID
/ * *
* Stores distribution lists , which represent different sets of people you may want to share a story with .
* /
2022-11-29 15:47:12 +00:00
class DistributionListTables constructor ( context : Context ? , databaseHelper : SignalDatabase ? ) : DatabaseTable ( context , databaseHelper ) , RecipientIdDatabaseReference {
2022-02-24 17:40:28 +00:00
companion object {
2022-11-29 15:47:12 +00:00
private val TAG = Log . tag ( DistributionListTables :: class . java )
2022-03-25 17:27:03 +00:00
2022-02-24 17:40:28 +00:00
@JvmField
val CREATE _TABLE : Array < String > = arrayOf ( ListTable . CREATE _TABLE , MembershipTable . CREATE _TABLE )
2022-06-24 14:51:26 +00:00
@JvmField
val CREATE _INDEXES : Array < String > = arrayOf ( MembershipTable . CREATE _INDEX )
2022-02-24 17:40:28 +00:00
const val RECIPIENT _ID = ListTable . RECIPIENT _ID
2022-05-10 14:01:51 +00:00
const val DISTRIBUTION _ID = ListTable . DISTRIBUTION _ID
const val LIST _TABLE _NAME = ListTable . TABLE _NAME
2022-08-18 13:11:42 +00:00
const val PRIVACY _MODE = ListTable . PRIVACY _MODE
2022-02-24 17:40:28 +00:00
fun insertInitialDistributionListAtCreationTime ( db : net . zetetic . database . sqlcipher . SQLiteDatabase ) {
val recipientId = db . insert (
2023-02-06 15:04:40 +00:00
RecipientTable . TABLE _NAME ,
null ,
2022-02-24 17:40:28 +00:00
contentValuesOf (
2022-11-29 15:47:12 +00:00
RecipientTable . GROUP _TYPE to RecipientTable . GroupType . DISTRIBUTION_LIST . id ,
RecipientTable . DISTRIBUTION _LIST _ID to DistributionListId . MY _STORY _ID ,
RecipientTable . STORAGE _SERVICE _ID to Base64 . encodeBytes ( StorageSyncHelper . generateKey ( ) ) ,
RecipientTable . PROFILE _SHARING to 1
2022-02-24 17:40:28 +00:00
)
)
db . insert (
2023-02-06 15:04:40 +00:00
ListTable . TABLE _NAME ,
null ,
2022-02-24 17:40:28 +00:00
contentValuesOf (
ListTable . ID to DistributionListId . MY _STORY _ID ,
2022-03-25 17:27:03 +00:00
ListTable . NAME to DistributionId . MY_STORY . toString ( ) ,
ListTable . DISTRIBUTION _ID to DistributionId . MY_STORY . toString ( ) ,
2022-06-24 14:51:26 +00:00
ListTable . RECIPIENT _ID to recipientId ,
ListTable . PRIVACY _MODE to DistributionListPrivacyMode . ALL . serialize ( )
2022-02-24 17:40:28 +00:00
)
)
}
}
private object ListTable {
const val TABLE _NAME = " distribution_list "
const val ID = " _id "
const val NAME = " name "
const val DISTRIBUTION _ID = " distribution_id "
const val RECIPIENT _ID = " recipient_id "
2022-03-01 16:41:45 +00:00
const val ALLOWS _REPLIES = " allows_replies "
2022-03-25 17:27:03 +00:00
const val DELETION _TIMESTAMP = " deletion_timestamp "
2022-05-10 14:01:51 +00:00
const val IS _UNKNOWN = " is_unknown "
2022-06-24 14:51:26 +00:00
const val PRIVACY _MODE = " privacy_mode "
2022-02-24 17:40:28 +00:00
2022-06-24 14:51:26 +00:00
val CREATE _TABLE = """
2022-02-24 17:40:28 +00:00
CREATE TABLE $ TABLE _NAME (
$ ID INTEGER PRIMARY KEY AUTOINCREMENT ,
$ NAME TEXT UNIQUE NOT NULL ,
$ DISTRIBUTION _ID TEXT UNIQUE NOT NULL ,
2022-11-29 15:47:12 +00:00
$ RECIPIENT _ID INTEGER UNIQUE REFERENCES $ { RecipientTable . TABLE _NAME } ( $ { RecipientTable . ID } ) ,
2022-03-25 17:27:03 +00:00
$ ALLOWS _REPLIES INTEGER DEFAULT 1 ,
2022-05-10 14:01:51 +00:00
$ DELETION _TIMESTAMP INTEGER DEFAULT 0 ,
2022-06-24 14:51:26 +00:00
$ IS _UNKNOWN INTEGER DEFAULT 0 ,
$ PRIVACY _MODE INTEGER DEFAULT $ { DistributionListPrivacyMode . ONLY_WITH . serialize ( ) }
2022-02-24 17:40:28 +00:00
)
"""
2022-03-25 17:27:03 +00:00
const val IS _NOT _DELETED = " $DELETION _TIMESTAMP == 0 "
2022-06-24 14:51:26 +00:00
2022-10-07 19:09:16 +00:00
val SEARCH _NAME _COLUMN = " search_name "
private val SEARCH _NAME = " LOWER( $NAME ) AS $SEARCH _NAME_COLUMN "
val LIST _UI _PROJECTION = arrayOf ( ID , NAME , RECIPIENT _ID , ALLOWS _REPLIES , IS _UNKNOWN , PRIVACY _MODE , SEARCH _NAME )
2022-02-24 17:40:28 +00:00
}
private object MembershipTable {
const val TABLE _NAME = " distribution_list_member "
const val ID = " _id "
const val LIST _ID = " list_id "
const val RECIPIENT _ID = " recipient_id "
2022-06-24 14:51:26 +00:00
const val PRIVACY _MODE = " privacy_mode "
2022-02-24 17:40:28 +00:00
const val CREATE _TABLE = """
CREATE TABLE $ TABLE _NAME (
$ ID INTEGER PRIMARY KEY AUTOINCREMENT ,
$ LIST _ID INTEGER NOT NULL REFERENCES $ { ListTable . TABLE _NAME } ( $ { ListTable . ID } ) ON DELETE CASCADE ,
2022-11-29 15:47:12 +00:00
$ RECIPIENT _ID INTEGER NOT NULL REFERENCES $ { RecipientTable . TABLE _NAME } ( $ { RecipientTable . ID } ) ,
2022-06-24 14:51:26 +00:00
$ PRIVACY _MODE INTEGER DEFAULT 0
2022-02-24 17:40:28 +00:00
)
"""
2022-06-24 14:51:26 +00:00
const val CREATE _INDEX = " CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON $TABLE _NAME ( $LIST _ID, $RECIPIENT _ID, $PRIVACY _MODE) "
2022-02-24 17:40:28 +00:00
}
/ * *
* @return true if the name change happened , false otherwise .
* /
fun setName ( distributionListId : DistributionListId , name : String ) : Boolean {
val db = writableDatabase
return db . updateWithOnConflict (
ListTable . TABLE _NAME ,
contentValuesOf ( ListTable . NAME to name ) ,
ID _WHERE ,
SqlUtil . buildArgs ( distributionListId ) ,
SQLiteDatabase . CONFLICT _IGNORE
) == 1
}
2022-06-24 14:51:26 +00:00
fun setPrivacyMode ( distributionListId : DistributionListId , privacyMode : DistributionListPrivacyMode ) {
val values = contentValuesOf ( ListTable . PRIVACY _MODE to privacyMode . serialize ( ) )
writableDatabase . update ( ListTable . TABLE _NAME , values , " ${ListTable.ID} = ? " , SqlUtil . buildArgs ( distributionListId ) )
2022-02-24 17:40:28 +00:00
}
fun getAllListsForContactSelectionUiCursor ( query : String ? , includeMyStory : Boolean ) : Cursor ? {
val db = readableDatabase
val where = when {
2022-03-25 17:27:03 +00:00
query . isNullOrEmpty ( ) && includeMyStory -> ListTable . IS _NOT _DELETED
query . isNullOrEmpty ( ) -> " ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED} "
2022-10-07 19:09:16 +00:00
includeMyStory -> " ( ${ListTable.SEARCH_NAME_COLUMN} GLOB ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN} "
else -> " ${ListTable.SEARCH_NAME_COLUMN} GLOB ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN} "
2022-02-24 17:40:28 +00:00
}
val whereArgs = when {
query . isNullOrEmpty ( ) && includeMyStory -> null
query . isNullOrEmpty ( ) -> SqlUtil . buildArgs ( DistributionListId . MY _STORY _ID )
2022-04-06 19:23:33 +00:00
else -> SqlUtil . buildArgs ( SqlUtil . buildCaseInsensitiveGlobPattern ( query ) , DistributionListId . MY _STORY _ID )
2022-02-24 17:40:28 +00:00
}
2022-06-24 14:51:26 +00:00
return db . query ( ListTable . TABLE _NAME , ListTable . LIST _UI _PROJECTION , where , whereArgs , null , null , null )
}
fun getAllListRecipients ( ) : List < RecipientId > {
return readableDatabase
. select ( ListTable . RECIPIENT _ID )
. from ( ListTable . TABLE _NAME )
. run ( )
. readToList { cursor -> RecipientId . from ( cursor . requireLong ( ListTable . RECIPIENT _ID ) ) }
2022-02-24 17:40:28 +00:00
}
2022-05-10 14:01:51 +00:00
/ * *
* Gets or creates a distribution list for the given id .
*
* If the list does not exist , then a new list is created with a randomized name and populated with the members
* in the manifest .
*
* @return the recipient id of the list
* /
fun getOrCreateByDistributionId ( distributionId : DistributionId , manifest : SentStorySyncManifest ) : RecipientId {
writableDatabase . beginTransaction ( )
try {
val distributionRecipientId = getRecipientIdByDistributionId ( distributionId )
if ( distributionRecipientId == null ) {
val members : List < RecipientId > = manifest . entries
. filter { it . distributionLists . contains ( distributionId ) }
. map { it . recipientId }
val distributionListId = createList (
name = createUniqueNameForUnknownDistributionId ( ) ,
members = members ,
distributionId = distributionId ,
isUnknown = true
)
if ( distributionListId == null ) {
throw AssertionError ( " Failed to create distribution list for unknown id. " )
} else {
val recipient = getRecipientId ( distributionListId )
if ( recipient == null ) {
throw AssertionError ( " Failed to retrieve recipient for newly created list " )
} else {
writableDatabase . setTransactionSuccessful ( )
return recipient
}
}
}
writableDatabase . setTransactionSuccessful ( )
return distributionRecipientId
} finally {
writableDatabase . endTransaction ( )
}
}
2022-02-24 17:40:28 +00:00
/ * *
* @return The id of the list if successful , otherwise null . If not successful , you can assume it was a name conflict .
* /
2022-03-25 17:27:03 +00:00
fun createList (
name : String ,
members : List < RecipientId > ,
distributionId : DistributionId = DistributionId . from ( UUID . randomUUID ( ) ) ,
allowsReplies : Boolean = true ,
2022-05-02 15:00:38 +00:00
deletionTimestamp : Long = 0L ,
2022-05-10 14:01:51 +00:00
storageId : ByteArray ? = null ,
2022-06-24 14:51:26 +00:00
isUnknown : Boolean = false ,
privacyMode : DistributionListPrivacyMode = DistributionListPrivacyMode . ONLY _WITH
2022-03-25 17:27:03 +00:00
) : DistributionListId ? {
2022-02-24 17:40:28 +00:00
val db = writableDatabase
db . beginTransaction ( )
try {
val values = ContentValues ( ) . apply {
2022-03-25 17:27:03 +00:00
put ( ListTable . NAME , if ( deletionTimestamp == 0L ) name else createUniqueNameForDeletedStory ( ) )
put ( ListTable . DISTRIBUTION _ID , distributionId . toString ( ) )
put ( ListTable . ALLOWS _REPLIES , if ( deletionTimestamp == 0L ) allowsReplies else false )
2022-02-24 17:40:28 +00:00
putNull ( ListTable . RECIPIENT _ID )
2022-03-25 17:27:03 +00:00
put ( ListTable . DELETION _TIMESTAMP , deletionTimestamp )
2022-05-10 14:01:51 +00:00
put ( ListTable . IS _UNKNOWN , isUnknown )
2022-06-24 14:51:26 +00:00
put ( ListTable . PRIVACY _MODE , privacyMode . serialize ( ) )
2022-02-24 17:40:28 +00:00
}
val id = writableDatabase . insert ( ListTable . TABLE _NAME , null , values )
if ( id < 0 ) {
return null
}
2022-05-02 15:00:38 +00:00
val recipientId = SignalDatabase . recipients . getOrInsertFromDistributionListId ( DistributionListId . from ( id ) , storageId )
2022-02-24 17:40:28 +00:00
writableDatabase . update (
ListTable . TABLE _NAME ,
ContentValues ( ) . apply { put ( ListTable . RECIPIENT _ID , recipientId . serialize ( ) ) } ,
" ${ListTable.ID} = ? " ,
SqlUtil . buildArgs ( id )
)
2022-06-24 14:51:26 +00:00
members . forEach { addMemberToList ( DistributionListId . from ( id ) , privacyMode , it ) }
2022-02-24 17:40:28 +00:00
db . setTransactionSuccessful ( )
return DistributionListId . from ( id )
} finally {
db . endTransaction ( )
}
}
2022-05-10 14:01:51 +00:00
fun getRecipientIdByDistributionId ( distributionId : DistributionId ) : RecipientId ? {
return readableDatabase
. select ( ListTable . RECIPIENT _ID )
. from ( ListTable . TABLE _NAME )
. where ( " ${ListTable.DISTRIBUTION_ID} = ? " , distributionId . toString ( ) )
. run ( )
. use { cursor ->
if ( cursor . moveToFirst ( ) ) {
RecipientId . from ( CursorUtil . requireLong ( cursor , ListTable . RECIPIENT _ID ) )
} else {
null
}
}
}
2022-03-01 16:41:45 +00:00
fun getStoryType ( listId : DistributionListId ) : StoryType {
2022-03-25 17:27:03 +00:00
readableDatabase . query ( ListTable . TABLE _NAME , arrayOf ( ListTable . ALLOWS _REPLIES ) , " ${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( listId ) , null , null , null ) . use { cursor ->
2022-03-01 16:41:45 +00:00
return if ( cursor . moveToFirst ( ) ) {
if ( CursorUtil . requireBoolean ( cursor , ListTable . ALLOWS _REPLIES ) ) {
StoryType . STORY _WITH _REPLIES
} else {
StoryType . STORY _WITHOUT _REPLIES
}
} else {
error ( " Distribution list not in database. " )
}
}
}
fun setAllowsReplies ( listId : DistributionListId , allowsReplies : Boolean ) {
2022-03-25 17:27:03 +00:00
writableDatabase . update ( ListTable . TABLE _NAME , contentValuesOf ( ListTable . ALLOWS _REPLIES to allowsReplies ) , " ${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( listId ) )
2022-03-01 16:41:45 +00:00
}
2022-02-24 17:40:28 +00:00
fun getList ( listId : DistributionListId ) : DistributionListRecord ? {
2022-08-11 17:37:37 +00:00
return getListByQuery ( " ${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( listId ) )
}
fun getList ( recipientId : RecipientId ) : DistributionListRecord ? {
return getListByQuery ( " ${ListTable.RECIPIENT_ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( recipientId ) )
}
fun getListByDistributionId ( distributionId : DistributionId ) : DistributionListRecord ? {
return getListByQuery ( " ${ListTable.DISTRIBUTION_ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( distributionId ) )
}
private fun getListByQuery ( query : String , args : Array < String > ) : DistributionListRecord ? {
readableDatabase . query ( ListTable . TABLE _NAME , null , query , args , null , null , null ) . use { cursor ->
2022-03-25 17:27:03 +00:00
return if ( cursor . moveToFirst ( ) ) {
val id : DistributionListId = DistributionListId . from ( cursor . requireLong ( ListTable . ID ) )
2022-06-24 14:51:26 +00:00
val privacyMode : DistributionListPrivacyMode = cursor . requireObject ( ListTable . PRIVACY _MODE , DistributionListPrivacyMode . Serializer )
2022-03-25 17:27:03 +00:00
DistributionListRecord (
id = id ,
name = cursor . requireNonNullString ( ListTable . NAME ) ,
distributionId = DistributionId . from ( cursor . requireNonNullString ( ListTable . DISTRIBUTION _ID ) ) ,
allowsReplies = CursorUtil . requireBoolean ( cursor , ListTable . ALLOWS _REPLIES ) ,
2022-06-24 14:51:26 +00:00
rawMembers = getRawMembers ( id , privacyMode ) ,
2022-03-25 17:27:03 +00:00
members = getMembers ( id ) ,
2022-05-10 14:01:51 +00:00
deletedAtTimestamp = 0L ,
2022-06-24 14:51:26 +00:00
isUnknown = CursorUtil . requireBoolean ( cursor , ListTable . IS _UNKNOWN ) ,
privacyMode = privacyMode
2022-03-25 17:27:03 +00:00
)
} else {
null
}
}
}
2022-08-15 17:34:35 +00:00
/ * *
* Gets the raw string value of distribution ID of the desired row . Added for additional logging around the UUID issues we ' ve seen .
* /
fun getRawDistributionId ( listId : DistributionListId ) : String ? {
return readableDatabase
. select ( ListTable . DISTRIBUTION _ID )
. from ( ListTable . TABLE _NAME )
. where ( " ${ListTable.ID} = ? " , listId )
. run ( )
. use { cursor ->
if ( cursor . moveToFirst ( ) ) {
cursor . requireString ( ListTable . DISTRIBUTION _ID )
} else {
null
}
}
}
2022-03-25 17:27:03 +00:00
fun getListForStorageSync ( listId : DistributionListId ) : DistributionListRecord ? {
2022-02-24 17:40:28 +00:00
readableDatabase . query ( ListTable . TABLE _NAME , null , " ${ListTable.ID} = ? " , SqlUtil . buildArgs ( listId ) , null , null , null ) . use { cursor ->
return if ( cursor . moveToFirst ( ) ) {
val id : DistributionListId = DistributionListId . from ( cursor . requireLong ( ListTable . ID ) )
2022-06-24 14:51:26 +00:00
val privacyMode = cursor . requireObject ( ListTable . PRIVACY _MODE , DistributionListPrivacyMode . Serializer )
2022-02-24 17:40:28 +00:00
DistributionListRecord (
id = id ,
name = cursor . requireNonNullString ( ListTable . NAME ) ,
distributionId = DistributionId . from ( cursor . requireNonNullString ( ListTable . DISTRIBUTION _ID ) ) ,
2022-03-01 16:41:45 +00:00
allowsReplies = CursorUtil . requireBoolean ( cursor , ListTable . ALLOWS _REPLIES ) ,
2022-06-24 14:51:26 +00:00
rawMembers = getRawMembers ( id , privacyMode ) ,
members = emptyList ( ) ,
2022-05-10 14:01:51 +00:00
deletedAtTimestamp = cursor . requireLong ( ListTable . DELETION _TIMESTAMP ) ,
2022-06-24 14:51:26 +00:00
isUnknown = CursorUtil . requireBoolean ( cursor , ListTable . IS _UNKNOWN ) ,
privacyMode = privacyMode
2022-02-24 17:40:28 +00:00
)
} else {
null
}
}
}
fun getDistributionId ( listId : DistributionListId ) : DistributionId ? {
2022-03-25 17:27:03 +00:00
readableDatabase . query ( ListTable . TABLE _NAME , arrayOf ( ListTable . DISTRIBUTION _ID ) , " ${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( listId ) , null , null , null ) . use { cursor ->
2022-02-24 17:40:28 +00:00
return if ( cursor . moveToFirst ( ) ) {
DistributionId . from ( cursor . requireString ( ListTable . DISTRIBUTION _ID ) )
} else {
null
}
}
}
2022-08-11 17:37:37 +00:00
fun getDistributionId ( recipientId : RecipientId ) : DistributionId ? {
readableDatabase . query ( ListTable . TABLE _NAME , arrayOf ( ListTable . DISTRIBUTION _ID ) , " ${ListTable.RECIPIENT_ID} = ? AND ${ListTable.IS_NOT_DELETED} " , SqlUtil . buildArgs ( recipientId ) , null , null , null ) . use { cursor ->
return if ( cursor . moveToFirst ( ) ) {
DistributionId . from ( cursor . requireString ( ListTable . DISTRIBUTION _ID ) )
} else {
null
}
}
}
2022-02-24 17:40:28 +00:00
fun getMembers ( listId : DistributionListId ) : List < RecipientId > {
2022-06-24 14:51:26 +00:00
lateinit var privacyMode : DistributionListPrivacyMode
lateinit var rawMembers : List < RecipientId >
readableDatabase . withinTransaction {
privacyMode = getPrivacyMode ( listId )
rawMembers = getRawMembers ( listId , privacyMode )
}
return when ( privacyMode ) {
DistributionListPrivacyMode . ALL -> {
SignalDatabase . recipients
. getSignalContacts ( false ) !!
2022-11-29 15:47:12 +00:00
. readToList { it . requireObject ( RecipientTable . ID , RecipientId . SERIALIZER ) }
2022-06-24 14:51:26 +00:00
}
DistributionListPrivacyMode . ALL _EXCEPT -> {
SignalDatabase . recipients
. getSignalContacts ( false ) !!
. readToList (
predicate = { ! rawMembers . contains ( it ) } ,
2022-11-29 15:47:12 +00:00
mapper = { it . requireObject ( RecipientTable . ID , RecipientId . SERIALIZER ) }
2022-06-24 14:51:26 +00:00
)
}
DistributionListPrivacyMode . ONLY _WITH -> rawMembers
2022-02-24 17:40:28 +00:00
}
}
2022-06-24 14:51:26 +00:00
fun getRawMembers ( listId : DistributionListId , privacyMode : DistributionListPrivacyMode ) : List < RecipientId > {
2022-02-24 17:40:28 +00:00
val members = mutableListOf < RecipientId > ( )
2022-06-24 14:51:26 +00:00
readableDatabase . query ( MembershipTable . TABLE _NAME , null , " ${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ? " , SqlUtil . buildArgs ( listId , privacyMode . serialize ( ) ) , null , null , null ) . use { cursor ->
2022-02-24 17:40:28 +00:00
while ( cursor . moveToNext ( ) ) {
members . add ( RecipientId . from ( cursor . requireLong ( MembershipTable . RECIPIENT _ID ) ) )
}
}
return members
}
fun getMemberCount ( listId : DistributionListId ) : Int {
2022-06-24 14:51:26 +00:00
return getPrivacyData ( listId ) . memberCount
}
fun getPrivacyData ( listId : DistributionListId ) : DistributionListPrivacyData {
lateinit var privacyMode : DistributionListPrivacyMode
var rawMemberCount = 0
var totalContactCount = 0
readableDatabase . withinTransaction {
privacyMode = getPrivacyMode ( listId )
rawMemberCount = getRawMemberCount ( listId , privacyMode )
totalContactCount = SignalDatabase . recipients . getSignalContactsCount ( false )
}
val memberCount = when ( privacyMode ) {
DistributionListPrivacyMode . ALL -> totalContactCount
2022-11-17 21:22:05 +00:00
DistributionListPrivacyMode . ALL _EXCEPT -> rawMemberCount
2022-06-24 14:51:26 +00:00
DistributionListPrivacyMode . ONLY _WITH -> rawMemberCount
2022-02-24 17:40:28 +00:00
}
2022-06-24 14:51:26 +00:00
return DistributionListPrivacyData (
privacyMode = privacyMode ,
memberCount = memberCount
)
2022-02-24 17:40:28 +00:00
}
2022-06-24 14:51:26 +00:00
private fun getRawMemberCount ( listId : DistributionListId , privacyMode : DistributionListPrivacyMode ) : Int {
readableDatabase . query ( MembershipTable . TABLE _NAME , SqlUtil . buildArgs ( " COUNT(*) " ) , " ${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ? " , SqlUtil . buildArgs ( listId , privacyMode . serialize ( ) ) , null , null , null ) . use { cursor ->
2022-02-24 17:40:28 +00:00
return if ( cursor . moveToFirst ( ) ) {
cursor . getInt ( 0 )
} else {
0
}
}
}
2022-06-24 14:51:26 +00:00
private fun getPrivacyMode ( listId : DistributionListId ) : DistributionListPrivacyMode {
return readableDatabase
. select ( ListTable . PRIVACY _MODE )
. from ( ListTable . TABLE _NAME )
. where ( " ${ListTable.ID} = ? " , listId . serialize ( ) )
. run ( )
. use {
if ( it . moveToFirst ( ) ) {
it . requireObject ( ListTable . PRIVACY _MODE , DistributionListPrivacyMode . Serializer )
} else {
DistributionListPrivacyMode . ONLY _WITH
}
}
}
fun removeMemberFromList ( listId : DistributionListId , privacyMode : DistributionListPrivacyMode , member : RecipientId ) {
writableDatabase . delete ( MembershipTable . TABLE _NAME , " ${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ? " , SqlUtil . buildArgs ( listId , member , privacyMode . serialize ( ) ) )
2022-02-24 17:40:28 +00:00
}
2022-06-24 14:51:26 +00:00
fun addMemberToList ( listId : DistributionListId , privacyMode : DistributionListPrivacyMode , member : RecipientId ) {
2022-02-24 17:40:28 +00:00
val values = ContentValues ( ) . apply {
put ( MembershipTable . LIST _ID , listId . serialize ( ) )
put ( MembershipTable . RECIPIENT _ID , member . serialize ( ) )
2022-06-24 14:51:26 +00:00
put ( MembershipTable . PRIVACY _MODE , privacyMode . serialize ( ) )
2022-02-24 17:40:28 +00:00
}
writableDatabase . insert ( MembershipTable . TABLE _NAME , null , values )
}
2022-06-24 14:51:26 +00:00
fun removeAllMembers ( listId : DistributionListId ) {
writableDatabase
. delete ( MembershipTable . TABLE _NAME )
. where ( " ${MembershipTable.LIST_ID} = ? " , listId . serialize ( ) )
. run ( )
}
2022-07-11 15:54:30 +00:00
fun removeAllMembers ( listId : DistributionListId , privacyMode : DistributionListPrivacyMode ) {
writableDatabase
. delete ( MembershipTable . TABLE _NAME )
. where ( " ${MembershipTable.LIST_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ? " , listId . serialize ( ) , privacyMode . serialize ( ) )
. run ( )
}
2022-09-27 12:26:44 +00:00
override fun remapRecipient ( oldId : RecipientId , newId : RecipientId ) {
2022-02-24 17:40:28 +00:00
val values = ContentValues ( ) . apply {
put ( MembershipTable . RECIPIENT _ID , newId . serialize ( ) )
}
writableDatabase . update ( MembershipTable . TABLE _NAME , values , " ${MembershipTable.RECIPIENT_ID} = ? " , SqlUtil . buildArgs ( oldId ) )
}
2022-03-25 17:27:03 +00:00
fun deleteList ( distributionListId : DistributionListId , deletionTimestamp : Long = System . currentTimeMillis ( ) ) {
writableDatabase . update (
ListTable . TABLE _NAME ,
contentValuesOf (
ListTable . NAME to createUniqueNameForDeletedStory ( ) ,
ListTable . ALLOWS _REPLIES to false ,
ListTable . DELETION _TIMESTAMP to deletionTimestamp
) ,
ID _WHERE ,
SqlUtil . buildArgs ( distributionListId )
)
writableDatabase . delete (
MembershipTable . TABLE _NAME ,
" ${MembershipTable.LIST_ID} = ? " ,
SqlUtil . buildArgs ( distributionListId )
)
}
fun getRecipientIdForSyncRecord ( record : SignalStoryDistributionListRecord ) : RecipientId ? {
2022-08-04 12:50:40 +00:00
val uuid : UUID = requireNotNull ( UuidUtil . parseOrNull ( record . identifier ) ) { " Incoming record did not have a valid identifier. " }
2022-03-25 17:27:03 +00:00
val distributionId = DistributionId . from ( uuid )
return readableDatabase . query (
ListTable . TABLE _NAME ,
arrayOf ( ListTable . RECIPIENT _ID ) ,
" ${ListTable.DISTRIBUTION_ID} = ? " ,
SqlUtil . buildArgs ( distributionId . toString ( ) ) ,
2023-02-06 15:04:40 +00:00
null ,
null ,
null
2022-03-25 17:27:03 +00:00
) ?. use { cursor ->
if ( cursor . moveToFirst ( ) ) {
RecipientId . from ( CursorUtil . requireLong ( cursor , ListTable . RECIPIENT _ID ) )
} else {
null
}
}
}
fun getRecipientId ( distributionListId : DistributionListId ) : RecipientId ? {
return readableDatabase . query (
ListTable . TABLE _NAME ,
arrayOf ( ListTable . RECIPIENT _ID ) ,
" ${ListTable.ID} = ? " ,
SqlUtil . buildArgs ( distributionListId ) ,
2023-02-06 15:04:40 +00:00
null ,
null ,
null
2022-03-25 17:27:03 +00:00
) ?. use { cursor ->
if ( cursor . moveToFirst ( ) ) {
RecipientId . from ( CursorUtil . requireLong ( cursor , ListTable . RECIPIENT _ID ) )
} else {
null
}
}
}
fun applyStorageSyncStoryDistributionListInsert ( insert : SignalStoryDistributionListRecord ) {
2022-04-04 16:14:28 +00:00
val distributionId = DistributionId . from ( UuidUtil . parseOrThrow ( insert . identifier ) )
if ( distributionId == DistributionId . MY _STORY ) {
throw AssertionError ( " Should never try to insert My Story " )
}
2022-06-24 14:51:26 +00:00
val privacyMode : DistributionListPrivacyMode = when {
insert . isBlockList && insert . recipients . isEmpty ( ) -> DistributionListPrivacyMode . ALL
insert . isBlockList -> DistributionListPrivacyMode . ALL _EXCEPT
else -> DistributionListPrivacyMode . ONLY _WITH
}
2022-03-25 17:27:03 +00:00
createList (
name = insert . name ,
members = insert . recipients . map ( RecipientId :: from ) ,
2022-04-04 16:14:28 +00:00
distributionId = distributionId ,
2022-03-25 17:27:03 +00:00
allowsReplies = insert . allowsReplies ( ) ,
2022-05-02 15:00:38 +00:00
deletionTimestamp = insert . deletedAtTimestamp ,
2022-06-24 14:51:26 +00:00
privacyMode = privacyMode ,
2022-05-02 15:00:38 +00:00
storageId = insert . id . raw
2022-03-25 17:27:03 +00:00
)
}
fun applyStorageSyncStoryDistributionListUpdate ( update : StorageRecordUpdate < SignalStoryDistributionListRecord > ) {
val distributionId = DistributionId . from ( UuidUtil . parseOrThrow ( update . new . identifier ) )
val distributionListId : DistributionListId ? = readableDatabase . query ( ListTable . TABLE _NAME , arrayOf ( ListTable . ID ) , " ${ListTable.DISTRIBUTION_ID} = ? " , SqlUtil . buildArgs ( distributionId . toString ( ) ) , null , null , null ) . use { cursor ->
if ( cursor == null || ! cursor . moveToFirst ( ) ) {
null
} else {
DistributionListId . from ( CursorUtil . requireLong ( cursor , ListTable . ID ) )
}
}
if ( distributionListId == null ) {
Log . w ( TAG , " Cannot find required distribution list. " )
return
}
2022-05-04 14:08:40 +00:00
val recipientId = getRecipientId ( distributionListId ) !!
SignalDatabase . recipients . updateStorageId ( recipientId , update . new . id . raw )
2022-03-25 17:27:03 +00:00
if ( update . new . deletedAtTimestamp > 0L ) {
2022-08-04 16:40:09 +00:00
if ( distributionId == DistributionId . MY _STORY ) {
2022-03-25 17:27:03 +00:00
Log . w ( TAG , " Refusing to delete My Story. " )
return
}
deleteList ( distributionListId , update . new . deletedAtTimestamp )
return
}
2022-06-24 14:51:26 +00:00
val privacyMode : DistributionListPrivacyMode = when {
update . new . isBlockList && update . new . recipients . isEmpty ( ) -> DistributionListPrivacyMode . ALL
update . new . isBlockList -> DistributionListPrivacyMode . ALL _EXCEPT
else -> DistributionListPrivacyMode . ONLY _WITH
}
writableDatabase . withinTransaction {
2022-03-25 17:27:03 +00:00
val listTableValues = contentValuesOf (
ListTable . ALLOWS _REPLIES to update . new . allowsReplies ( ) ,
2022-05-10 14:01:51 +00:00
ListTable . NAME to update . new . name ,
2022-06-24 14:51:26 +00:00
ListTable . IS _UNKNOWN to false ,
ListTable . PRIVACY _MODE to privacyMode . serialize ( )
2022-03-25 17:27:03 +00:00
)
writableDatabase . update (
ListTable . TABLE _NAME ,
listTableValues ,
" ${ListTable.DISTRIBUTION_ID} = ? " ,
SqlUtil . buildArgs ( distributionId . toString ( ) )
)
2022-06-24 14:51:26 +00:00
val currentlyInDistributionList = getRawMembers ( distributionListId , privacyMode ) . toSet ( )
2022-03-25 17:27:03 +00:00
val shouldBeInDistributionList = update . new . recipients . map ( RecipientId :: from ) . toSet ( )
val toRemove = currentlyInDistributionList - shouldBeInDistributionList
val toAdd = shouldBeInDistributionList - currentlyInDistributionList
toRemove . forEach {
2022-06-24 14:51:26 +00:00
removeMemberFromList ( distributionListId , privacyMode , it )
2022-03-25 17:27:03 +00:00
}
toAdd . forEach {
2022-06-24 14:51:26 +00:00
addMemberToList ( distributionListId , privacyMode , it )
2022-03-25 17:27:03 +00:00
}
}
}
private fun createUniqueNameForDeletedStory ( ) : String {
return " DELETED- ${UUID.randomUUID()} "
2022-02-24 17:40:28 +00:00
}
2022-05-10 14:01:51 +00:00
private fun createUniqueNameForUnknownDistributionId ( ) : String {
return " DELETED- ${UUID.randomUUID()} "
}
2022-07-11 15:54:30 +00:00
fun excludeFromStory ( recipientId : RecipientId , record : DistributionListRecord ) {
excludeAllFromStory ( listOf ( recipientId ) , record )
}
fun excludeAllFromStory ( recipientIds : List < RecipientId > , record : DistributionListRecord ) {
writableDatabase . withinTransaction {
when ( record . privacyMode ) {
DistributionListPrivacyMode . ONLY _WITH -> {
recipientIds . forEach {
removeMemberFromList ( record . id , record . privacyMode , it )
}
}
DistributionListPrivacyMode . ALL _EXCEPT -> {
recipientIds . forEach {
addMemberToList ( record . id , record . privacyMode , it )
}
}
DistributionListPrivacyMode . ALL -> {
removeAllMembers ( record . id , DistributionListPrivacyMode . ALL _EXCEPT )
setPrivacyMode ( record . id , DistributionListPrivacyMode . ALL _EXCEPT )
recipientIds . forEach {
addMemberToList ( record . id , DistributionListPrivacyMode . ALL _EXCEPT , it )
}
}
}
}
}
2022-02-24 17:40:28 +00:00
}