Implement StoryDistributionListRecord and processing.

This commit is contained in:
Alex Hart
2022-03-25 14:27:03 -03:00
committed by Cody Henthorne
parent 2cd7462573
commit c359b0134a
21 changed files with 896 additions and 64 deletions

View File

@@ -6,6 +6,7 @@ import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
@@ -14,9 +15,12 @@ import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Base64
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
/**
@@ -25,6 +29,8 @@ import java.util.UUID
class DistributionListDatabase constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
companion object {
private val TAG = Log.tag(DistributionListDatabase::class.java)
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
@@ -34,18 +40,18 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
val recipientId = db.insert(
RecipientDatabase.TABLE_NAME, null,
contentValuesOf(
RecipientDatabase.GROUP_TYPE to RecipientDatabase.GroupType.DISTRIBUTION_LIST.id,
RecipientDatabase.DISTRIBUTION_LIST_ID to DistributionListId.MY_STORY_ID,
RecipientDatabase.STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
RecipientDatabase.PROFILE_SHARING to 1
)
)
val listUUID = UUID.randomUUID().toString()
db.insert(
ListTable.TABLE_NAME, null,
contentValuesOf(
ListTable.ID to DistributionListId.MY_STORY_ID,
ListTable.NAME to listUUID,
ListTable.DISTRIBUTION_ID to listUUID,
ListTable.NAME to DistributionId.MY_STORY.toString(),
ListTable.DISTRIBUTION_ID to DistributionId.MY_STORY.toString(),
ListTable.RECIPIENT_ID to recipientId
)
)
@@ -60,6 +66,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
const val DISTRIBUTION_ID = "distribution_id"
const val RECIPIENT_ID = "recipient_id"
const val ALLOWS_REPLIES = "allows_replies"
const val DELETION_TIMESTAMP = "deletion_timestamp"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
@@ -67,9 +74,12 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
$NAME TEXT UNIQUE NOT NULL,
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
$ALLOWS_REPLIES INTEGER DEFAULT 1
$ALLOWS_REPLIES INTEGER DEFAULT 1,
$DELETION_TIMESTAMP INTEGER DEFAULT 0
)
"""
const val IS_NOT_DELETED = "$DELETION_TIMESTAMP == 0"
}
private object MembershipTable {
@@ -127,10 +137,10 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
val where = when {
query.isNullOrEmpty() && includeMyStory -> null
query.isNullOrEmpty() -> "${ListTable.ID} != ?"
includeMyStory -> "${ListTable.NAME} LIKE ? OR ${ListTable.ID} == ?"
else -> "${ListTable.NAME} LIKE ? AND ${ListTable.ID} != ?"
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
includeMyStory -> "(${ListTable.NAME} LIKE ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED}"
else -> "${ListTable.NAME} LIKE ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
}
val whereArgs = when {
@@ -145,7 +155,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
val db = readableDatabase
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID}"
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
val results = mutableListOf<DistributionListPartialRecord>()
@@ -167,15 +177,23 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
/**
* @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict.
*/
fun createList(name: String, members: List<RecipientId>): DistributionListId? {
fun createList(
name: String,
members: List<RecipientId>,
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
allowsReplies: Boolean = true,
deletionTimestamp: Long = 0L
): DistributionListId? {
val db = writableDatabase
db.beginTransaction()
try {
val values = ContentValues().apply {
put(ListTable.NAME, name)
put(ListTable.DISTRIBUTION_ID, UUID.randomUUID().toString())
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)
putNull(ListTable.RECIPIENT_ID)
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
}
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
@@ -203,7 +221,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}
fun getStoryType(listId: DistributionListId): StoryType {
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
if (CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES)) {
StoryType.STORY_WITH_REPLIES
@@ -217,10 +235,29 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}
fun setAllowsReplies(listId: DistributionListId, allowsReplies: Boolean) {
writableDatabase.update(ListTable.TABLE_NAME, contentValuesOf(ListTable.ALLOWS_REPLIES to allowsReplies), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId))
writableDatabase.update(ListTable.TABLE_NAME, contentValuesOf(ListTable.ALLOWS_REPLIES to allowsReplies), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId))
}
fun getList(listId: DistributionListId): DistributionListRecord? {
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
DistributionListRecord(
id = id,
name = cursor.requireNonNullString(ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
members = getMembers(id),
deletedAtTimestamp = 0L
)
} else {
null
}
}
}
fun getListForStorageSync(listId: DistributionListId): DistributionListRecord? {
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))
@@ -230,7 +267,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
name = cursor.requireNonNullString(ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
members = getMembers(id)
members = getRawMembers(id),
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP)
)
} else {
null
@@ -239,7 +277,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}
fun getDistributionId(listId: DistributionListId): DistributionId? {
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.DISTRIBUTION_ID), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.DISTRIBUTION_ID), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
DistributionId.from(cursor.requireString(ListTable.DISTRIBUTION_ID))
} else {
@@ -318,7 +356,130 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
writableDatabase.update(MembershipTable.TABLE_NAME, values, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(oldId))
}
fun deleteList(distributionListId: DistributionListId) {
writableDatabase.delete(ListTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(distributionListId))
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? {
val uuid: UUID = UuidUtil.parseOrNull(record.identifier) ?: return null
val distributionId = DistributionId.from(uuid)
return readableDatabase.query(
ListTable.TABLE_NAME,
arrayOf(ListTable.RECIPIENT_ID),
"${ListTable.DISTRIBUTION_ID} = ?",
SqlUtil.buildArgs(distributionId.toString()),
null, null, null
)?.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),
null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
RecipientId.from(CursorUtil.requireLong(cursor, ListTable.RECIPIENT_ID))
} else {
null
}
}
}
fun applyStorageSyncStoryDistributionListInsert(insert: SignalStoryDistributionListRecord) {
createList(
name = insert.name,
members = insert.recipients.map(RecipientId::from),
distributionId = DistributionId.from(UuidUtil.parseOrThrow(insert.identifier)),
allowsReplies = insert.allowsReplies(),
deletionTimestamp = insert.deletedAtTimestamp
)
}
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
}
if (update.new.deletedAtTimestamp > 0L) {
if (distributionId.asUuid().equals(DistributionId.MY_STORY.asUuid())) {
Log.w(TAG, "Refusing to delete My Story.")
return
}
deleteList(distributionListId, update.new.deletedAtTimestamp)
return
}
writableDatabase.beginTransaction()
try {
val listTableValues = contentValuesOf(
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
ListTable.NAME to update.new.name
)
writableDatabase.update(
ListTable.TABLE_NAME,
listTableValues,
"${ListTable.DISTRIBUTION_ID} = ?",
SqlUtil.buildArgs(distributionId.toString())
)
val currentlyInDistributionList = getRawMembers(distributionListId).toSet()
val shouldBeInDistributionList = update.new.recipients.map(RecipientId::from).toSet()
val toRemove = currentlyInDistributionList - shouldBeInDistributionList
val toAdd = shouldBeInDistributionList - currentlyInDistributionList
toRemove.forEach {
removeMemberFromList(distributionListId, it)
}
toAdd.forEach {
addMemberToList(distributionListId, it)
}
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
private fun createUniqueNameForDeletedStory(): String {
return "DELETED-${UUID.randomUUID()}"
}
}

View File

@@ -593,6 +593,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
DISTRIBUTION_LIST_ID,
distributionListId.serialize(),
ContentValues().apply {
put(GROUP_TYPE, GroupType.DISTRIBUTION_LIST.id)
put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
put(PROFILE_SHARING, 1)
@@ -1071,10 +1072,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
$STORAGE_SERVICE_ID NOT NULL AND (
($GROUP_TYPE = ? AND $SERVICE_ID NOT NULL AND $ID != ?)
OR
$GROUP_TYPE IN (?)
$GROUP_TYPE IN (?, ?)
)
""".trimIndent()
val args = SqlUtil.buildArgs(GroupType.NONE.id, Recipient.self().id, GroupType.SIGNAL_V1.id)
val args = SqlUtil.buildArgs(GroupType.NONE.id, Recipient.self().id, GroupType.SIGNAL_V1.id, GroupType.DISTRIBUTION_LIST.id)
val out: MutableMap<RecipientId, StorageId> = HashMap()
readableDatabase.query(TABLE_NAME, arrayOf(ID, STORAGE_SERVICE_ID, GROUP_TYPE), query, args, null, null, null).use { cursor ->
@@ -1087,6 +1088,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
when (groupType) {
GroupType.NONE -> out[id] = StorageId.forContact(key)
GroupType.SIGNAL_V1 -> out[id] = StorageId.forGroupV1(key)
GroupType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
else -> throw AssertionError()
}
}
@@ -2504,7 +2506,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?, ?) OR $REGISTERED = ?)"
val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, GroupType.DISTRIBUTION_LIST, RegisteredState.REGISTERED.id)
val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, GroupType.DISTRIBUTION_LIST.id, RegisteredState.REGISTERED.id)
writableDatabase.update(TABLE_NAME, values, query, args)
}

View File

@@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
@@ -196,8 +195,9 @@ object SignalDatabaseMigrations {
private const val GROUP_STORIES = 134
private const val MMS_COUNT_INDEX = 135
private const val STORY_SENDS = 136
private const val STORY_TYPE_AND_DISTRIBUTION = 137
const val DATABASE_VERSION = 136
const val DATABASE_VERSION = 137
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -2458,7 +2458,7 @@ object SignalDatabaseMigrations {
val recipientId = db.insert(
"recipient", null,
contentValuesOf(
"distribution_list_id" to DistributionListId.MY_STORY_ID,
"distribution_list_id" to 1L,
"storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()),
"profile_sharing" to 1
)
@@ -2468,7 +2468,7 @@ object SignalDatabaseMigrations {
db.insert(
"distribution_list", null,
contentValuesOf(
"_id" to DistributionListId.MY_STORY_ID,
"_id" to 1L,
"name" to listUUID,
"distribution_id" to listUUID,
"recipient_id" to recipientId
@@ -2503,6 +2503,27 @@ object SignalDatabaseMigrations {
db.execSQL("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")
}
if (oldVersion < STORY_TYPE_AND_DISTRIBUTION) {
db.execSQL("ALTER TABLE distribution_list ADD COLUMN deletion_timestamp INTEGER DEFAULT 0")
db.execSQL(
"""
UPDATE recipient
SET group_type = 4
WHERE distribution_list_id IS NOT NULL
""".trimIndent()
)
db.execSQL(
"""
UPDATE distribution_list
SET name = '00000000-0000-0000-0000-000000000000',
distribution_id = '00000000-0000-0000-0000-000000000000'
WHERE _id = 1
""".trimIndent()
)
}
}
@JvmStatic

View File

@@ -11,5 +11,6 @@ data class DistributionListRecord(
val name: String,
val distributionId: DistributionId,
val allowsReplies: Boolean,
val members: List<RecipientId>
val members: List<RecipientId>,
val deletedAtTimestamp: Long
)