kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement StoryDistributionListRecord and processing.
rodzic
2cd7462573
commit
c359b0134a
|
@ -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()}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult;
|
|||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||
import org.thoughtcrime.securesms.storage.StoryDistributionListRecordProcessor;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -48,10 +49,12 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
|||
import org.whispersystems.signalservice.api.storage.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -269,11 +272,12 @@ public class StorageSyncJob extends BaseJob {
|
|||
Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". These stragglers should naturally get deleted during the sync.");
|
||||
}
|
||||
|
||||
List<SignalContactRecord> remoteContacts = new LinkedList<>();
|
||||
List<SignalGroupV1Record> remoteGv1 = new LinkedList<>();
|
||||
List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
|
||||
List<SignalAccountRecord> remoteAccount = new LinkedList<>();
|
||||
List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
|
||||
List<SignalContactRecord> remoteContacts = new LinkedList<>();
|
||||
List<SignalGroupV1Record> remoteGv1 = new LinkedList<>();
|
||||
List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
|
||||
List<SignalAccountRecord> remoteAccount = new LinkedList<>();
|
||||
List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
|
||||
List<SignalStoryDistributionListRecord> remoteStoryDistributionLists = new LinkedList<>();
|
||||
|
||||
for (SignalStorageRecord remote : remoteOnly) {
|
||||
if (remote.getContact().isPresent()) {
|
||||
|
@ -284,6 +288,8 @@ public class StorageSyncJob extends BaseJob {
|
|||
remoteGv2.add(remote.getGroupV2().get());
|
||||
} else if (remote.getAccount().isPresent()) {
|
||||
remoteAccount.add(remote.getAccount().get());
|
||||
} else if (remote.getStoryDistributionList().isPresent()) {
|
||||
remoteStoryDistributionLists.add(remote.getStoryDistributionList().get());
|
||||
} else if (remote.getId().isUnknown()) {
|
||||
remoteUnknown.add(remote);
|
||||
} else {
|
||||
|
@ -302,6 +308,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
new GroupV2RecordProcessor(context).process(remoteGv2, StorageSyncHelper.KEY_GENERATOR);
|
||||
self = freshSelf();
|
||||
new AccountRecordProcessor(context, self).process(remoteAccount, StorageSyncHelper.KEY_GENERATOR);
|
||||
new StoryDistributionListRecordProcessor().process(remoteStoryDistributionLists, StorageSyncHelper.KEY_GENERATOR);
|
||||
|
||||
List<SignalStorageRecord> unknownInserts = remoteUnknown;
|
||||
List<StorageId> unknownDeletes = Stream.of(idDifference.getLocalOnlyIds()).filter(StorageId::isUnknown).toList();
|
||||
|
@ -424,6 +431,16 @@ public class StorageSyncJob extends BaseJob {
|
|||
}
|
||||
records.add(StorageSyncHelper.buildAccountRecord(context, self));
|
||||
break;
|
||||
case ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST_VALUE:
|
||||
RecipientRecord record = recipientDatabase.getByStorageId(id.getRaw());
|
||||
if (record != null) {
|
||||
if (record.getDistributionListId() != null) {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(record));
|
||||
} else {
|
||||
throw new MissingRecipientModelError("Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
||||
if (unknown != null) {
|
||||
|
|
|
@ -100,9 +100,10 @@ public class ApplicationMigrations {
|
|||
static final int PNI_IDENTITY = 56;
|
||||
static final int PNI_IDENTITY_2 = 57;
|
||||
static final int PNI_IDENTITY_3 = 58;
|
||||
static final int STORY_DISTRIBUTION_LIST_SYNC = 59;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 58;
|
||||
public static final int CURRENT_VERSION = 59;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
|
@ -436,6 +437,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.PNI_IDENTITY_3, new PniAccountInitializationMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.STORY_DISTRIBUTION_LIST_SYNC) {
|
||||
jobs.put(Version.STORY_DISTRIBUTION_LIST_SYNC, new StorageServiceMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,19 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord;
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -19,11 +25,14 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
|||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class StorageSyncModels {
|
||||
|
||||
|
@ -47,10 +56,11 @@ public final class StorageSyncModels {
|
|||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientRecord settings, @NonNull byte[] rawStorageId) {
|
||||
switch (settings.getGroupType()) {
|
||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId));
|
||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||
case SIGNAL_V2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
||||
default: throw new AssertionError("Unsupported type!");
|
||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId));
|
||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||
case SIGNAL_V2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
||||
case DISTRIBUTION_LIST: return SignalStorageRecord.forStoryDistributionList(localToRemoteStoryDistributionList(settings, rawStorageId));
|
||||
default: throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,6 +171,38 @@ public final class StorageSyncModels {
|
|||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalStoryDistributionListRecord localToRemoteStoryDistributionList(@NonNull RecipientRecord recipient, @NonNull byte[] rawStorageId) {
|
||||
DistributionListId distributionListId = recipient.getDistributionListId();
|
||||
|
||||
if (distributionListId == null) {
|
||||
throw new AssertionError("Must have a distributionListId!");
|
||||
}
|
||||
|
||||
DistributionListRecord record = SignalDatabase.distributionLists().getListForStorageSync(distributionListId);
|
||||
if (record == null) {
|
||||
throw new AssertionError("Must have a distribution list record!");
|
||||
}
|
||||
|
||||
if (record.getDeletedAtTimestamp() > 0L) {
|
||||
return new SignalStoryDistributionListRecord.Builder(rawStorageId, recipient.getSyncExtras().getStorageProto())
|
||||
.setIdentifier(UuidUtil.toByteArray(record.getDistributionId().asUuid()))
|
||||
.setDeletedAtTimestamp(record.getDeletedAtTimestamp())
|
||||
.build();
|
||||
}
|
||||
|
||||
return new SignalStoryDistributionListRecord.Builder(rawStorageId, recipient.getSyncExtras().getStorageProto())
|
||||
.setIdentifier(UuidUtil.toByteArray(record.getDistributionId().asUuid()))
|
||||
.setName(record.getName())
|
||||
.setRecipients(record.getMembers().stream()
|
||||
.map(Recipient::resolved)
|
||||
.filter(Recipient::hasServiceId)
|
||||
.map(Recipient::requireServiceId)
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList()))
|
||||
.setAllowsReplies(record.getAllowsReplies())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static @NonNull IdentityDatabase.VerifiedStatus remoteToLocalIdentityStatus(@NonNull IdentityState identityState) {
|
||||
switch (identityState) {
|
||||
case VERIFIED: return IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class StoryDistributionListRecordProcessor extends DefaultStorageRecordProcessor<SignalStoryDistributionListRecord> {
|
||||
|
||||
private static final String TAG = Log.tag(StoryDistributionListRecordProcessor.class);
|
||||
|
||||
private boolean haveSeenMyStory;
|
||||
|
||||
/**
|
||||
* At a minimum, we require:
|
||||
* <ul>
|
||||
* <li>A valid identifier</li>
|
||||
* <li>A non-visually-empty name field OR a deleted at timestamp</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
boolean isInvalid(@NonNull SignalStoryDistributionListRecord remote) {
|
||||
UUID remoteUuid = UuidUtil.parseOrNull(remote.getIdentifier());
|
||||
if (remoteUuid == null) {
|
||||
Log.d(TAG, "Bad distribution list identifier -- marking as invalid");
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isMyStory = remoteUuid.equals(DistributionId.MY_STORY.asUuid());
|
||||
if (haveSeenMyStory && isMyStory) {
|
||||
Log.w(TAG, "Found an additional MyStory record -- marking as invalid");
|
||||
return true;
|
||||
}
|
||||
|
||||
haveSeenMyStory |= isMyStory;
|
||||
|
||||
if (remote.getDeletedAtTimestamp() > 0L) {
|
||||
if (isMyStory) {
|
||||
Log.w(TAG, "Refusing to delete My Story -- marking as invalid");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtil.isVisuallyEmpty(remote.getName())) {
|
||||
Log.d(TAG, "Bad distribution list name (visually empty) -- marking as invalid");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull Optional<SignalStoryDistributionListRecord> getMatching(@NonNull SignalStoryDistributionListRecord remote, @NonNull StorageKeyGenerator keyGenerator) {
|
||||
RecipientId matching = SignalDatabase.distributionLists().getRecipientIdForSyncRecord(remote);
|
||||
if (matching != null) {
|
||||
RecipientRecord recordForSync = SignalDatabase.recipients().getRecordForSync(matching);
|
||||
if (recordForSync == null) {
|
||||
throw new IllegalStateException("Found matching recipient but couldn't generate record for sync.");
|
||||
}
|
||||
|
||||
return StorageSyncModels.localToRemoteRecord(recordForSync).getStoryDistributionList();
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull SignalStoryDistributionListRecord merge(@NonNull SignalStoryDistributionListRecord remote, @NonNull SignalStoryDistributionListRecord local, @NonNull StorageKeyGenerator keyGenerator) {
|
||||
byte[] unknownFields = remote.serializeUnknownFields();
|
||||
byte[] identifier = remote.getIdentifier();
|
||||
String name = remote.getName();
|
||||
List<SignalServiceAddress> recipients = remote.getRecipients();
|
||||
long deletedAtTimestamp = remote.getDeletedAtTimestamp();
|
||||
boolean allowsReplies = remote.allowsReplies();
|
||||
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalStoryDistributionListRecord.Builder(keyGenerator.generate(), unknownFields)
|
||||
.setIdentifier(identifier)
|
||||
.setName(name)
|
||||
.setRecipients(recipients)
|
||||
.setDeletedAtTimestamp(deletedAtTimestamp)
|
||||
.setAllowsReplies(allowsReplies)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void insertLocal(@NonNull SignalStoryDistributionListRecord record) throws IOException {
|
||||
SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListInsert(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateLocal(@NonNull StorageRecordUpdate<SignalStoryDistributionListRecord> update) {
|
||||
SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListUpdate(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(SignalStoryDistributionListRecord o1, SignalStoryDistributionListRecord o2) {
|
||||
if (Arrays.equals(o1.getIdentifier(), o2.getIdentifier())) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doParamsMatch(@NonNull SignalStoryDistributionListRecord record,
|
||||
@Nullable byte[] unknownFields,
|
||||
@Nullable byte[] identifier,
|
||||
@Nullable String name,
|
||||
@NonNull List<SignalServiceAddress> recipients,
|
||||
long deletedAtTimestamp,
|
||||
boolean allowsReplies) {
|
||||
return Arrays.equals(unknownFields, record.serializeUnknownFields()) &&
|
||||
Arrays.equals(identifier, record.getIdentifier()) &&
|
||||
Objects.equals(name, record.getName()) &&
|
||||
Objects.equals(recipients, record.getRecipients()) &&
|
||||
deletedAtTimestamp == record.getDeletedAtTimestamp() &&
|
||||
allowsReplies == record.allowsReplies();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import io.reactivex.rxjava3.core.Completable
|
|||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
|
@ -14,6 +15,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
|
@ -51,4 +53,16 @@ object Stories {
|
|||
|
||||
return RecipientUtil.getEligibleForSending(recipientIds.map(Recipient::resolved))
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun onStorySettingsChanged(distributionListId: DistributionListId) {
|
||||
val recipientId = SignalDatabase.distributionLists.getRecipientId(distributionListId) ?: error("Cannot find recipient id for distribution list.")
|
||||
onStorySettingsChanged(recipientId)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun onStorySettingsChanged(storyRecipientId: RecipientId) {
|
||||
SignalDatabase.recipients.markNeedsSync(storyRecipientId)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class CreateStoryWithViewersRepository {
|
||||
fun createList(name: CharSequence, members: Set<RecipientId>): Single<RecipientId> {
|
||||
|
@ -12,6 +13,7 @@ class CreateStoryWithViewersRepository {
|
|||
if (result == null) {
|
||||
it.onError(Exception("Null result, due to a duplicated name."))
|
||||
} else {
|
||||
Stories.onStorySettingsChanged(result)
|
||||
it.onSuccess(SignalDatabase.recipients.getOrInsertFromDistributionListId(result))
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
|||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class PrivateStorySettingsRepository {
|
||||
fun getRecord(distributionListId: DistributionListId): Single<DistributionListRecord> {
|
||||
|
@ -18,12 +19,14 @@ class PrivateStorySettingsRepository {
|
|||
fun removeMember(distributionListId: DistributionListId, member: RecipientId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListId, member)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun delete(distributionListId: DistributionListId): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.deleteList(distributionListId)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
@ -36,6 +39,7 @@ class PrivateStorySettingsRepository {
|
|||
fun setRepliesAndReactionsEnabled(distributionListId: DistributionListId, repliesAndReactionsEnabled: Boolean): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.setAllowsReplies(distributionListId, repliesAndReactionsEnabled)
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Completable
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class EditStoryNameRepository {
|
||||
fun save(privateStoryId: DistributionListId, name: CharSequence): Completable {
|
||||
|
@ -13,6 +14,8 @@ class EditStoryNameRepository {
|
|||
}
|
||||
|
||||
if (SignalDatabase.distributionLists.setName(privateStoryId, name.toString())) {
|
||||
Stories.onStorySettingsChanged(privateStoryId)
|
||||
|
||||
it.onComplete()
|
||||
} else {
|
||||
it.onError(Exception("Could not update story name."))
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class MyStorySettingsRepository {
|
||||
|
||||
|
@ -23,6 +24,7 @@ class MyStorySettingsRepository {
|
|||
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.distributionLists.setAllowsReplies(DistributionListId.MY_STORY, repliesAndReactionsEnabled)
|
||||
Stories.onStorySettingsChanged(DistributionListId.MY_STORY)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
|
||||
class BaseStoryRecipientSelectionRepository {
|
||||
fun updateDistributionListMembership(distributionListId: DistributionListId, recipients: Set<RecipientId>) {
|
||||
|
@ -23,6 +24,8 @@ class BaseStoryRecipientSelectionRepository {
|
|||
newNotOld.forEach {
|
||||
SignalDatabase.distributionLists.addMemberToList(distributionListId, it)
|
||||
}
|
||||
|
||||
Stories.onStorySettingsChanged(distributionListId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testutil.EmptyLogger
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
||||
import java.util.UUID
|
||||
|
||||
class StoryDistributionListRecordProcessorTest {
|
||||
|
||||
companion object {
|
||||
val STORAGE_ID: StorageId = StorageId.forStoryDistributionList(byteArrayOf(1, 2, 3, 4))
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpClass() {
|
||||
Log.initialize(EmptyLogger())
|
||||
}
|
||||
}
|
||||
|
||||
private val testSubject = StoryDistributionListRecordProcessor()
|
||||
|
||||
@Test
|
||||
fun `Given a proto without an identifier, when I isInvalid, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord.getDefaultInstance()
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto with an identifier that is not a UUID, when I isInvalid, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(ByteString.copyFrom("Greetings, fellow UUIDs".encodeToByteArray()))
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto without a name or deletion timestamp, when I isInvalid, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto with a deletion timestamp, when I isInvalid, then I expect false`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.setDeletedAtTimestamp(1)
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto that is MyStory with a deletion timestamp, when I isInvalid, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(DistributionId.MY_STORY.asUuid()))
|
||||
.setDeletedAtTimestamp(1)
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a validated proto that is MyStory, when I isInvalid with another MyStory, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(DistributionId.MY_STORY.asUuid()))
|
||||
.setDeletedAtTimestamp(1)
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
testSubject.isInvalid(record)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto with a visible name, when I isInvalid, then I expect false`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.setName("A visible name")
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto without a name, when I isInvalid, then I expect false`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a proto without a visible name, when I isInvalid, then I expect true`() {
|
||||
// GIVEN
|
||||
val proto = StoryDistributionListRecord
|
||||
.getDefaultInstance()
|
||||
.toBuilder()
|
||||
.setIdentifier(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.setName(" ")
|
||||
.build()
|
||||
|
||||
val record = SignalStoryDistributionListRecord(STORAGE_ID, proto)
|
||||
|
||||
// WHEN
|
||||
val result = testSubject.isInvalid(record)
|
||||
|
||||
// THEN
|
||||
assertTrue(result)
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ import java.util.UUID;
|
|||
*/
|
||||
public final class DistributionId {
|
||||
|
||||
public static final DistributionId MY_STORY = DistributionId.from("00000000-0000-0000-0000-000000000000");
|
||||
|
||||
private final UUID uuid;
|
||||
|
||||
public static DistributionId from(String id) {
|
||||
|
|
|
@ -44,6 +44,8 @@ public final class SignalStorageModels {
|
|||
return SignalStorageRecord.forGroupV2(id, new SignalGroupV2Record(id, record.getGroupV2()));
|
||||
} else if (record.hasAccount() && type == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) {
|
||||
return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.getAccount()));
|
||||
} else if (record.hasStoryDistributionList() && type == ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST_VALUE) {
|
||||
return SignalStorageRecord.forStoryDistributionList(id, new SignalStoryDistributionListRecord(id, record.getStoryDistributionList()));
|
||||
} else {
|
||||
if (StorageId.isKnownType(type)) {
|
||||
Log.w(TAG, "StorageId is of known type (" + type + "), but the data is bad! Falling back to unknown.");
|
||||
|
@ -63,6 +65,8 @@ public final class SignalStorageModels {
|
|||
builder.setGroupV2(record.getGroupV2().get().toProto());
|
||||
} else if (record.getAccount().isPresent()) {
|
||||
builder.setAccount(record.getAccount().get().toProto());
|
||||
} else if (record.getStoryDistributionList().isPresent()) {
|
||||
builder.setStoryDistributionList(record.getStoryDistributionList().get().toProto());
|
||||
} else {
|
||||
throw new InvalidStorageWriteError();
|
||||
}
|
||||
|
|
|
@ -6,18 +6,27 @@ import java.util.Optional;
|
|||
|
||||
public class SignalStorageRecord implements SignalRecord {
|
||||
|
||||
private final StorageId id;
|
||||
private final Optional<SignalContactRecord> contact;
|
||||
private final Optional<SignalGroupV1Record> groupV1;
|
||||
private final Optional<SignalGroupV2Record> groupV2;
|
||||
private final Optional<SignalAccountRecord> account;
|
||||
private final StorageId id;
|
||||
private final Optional<SignalStoryDistributionListRecord> storyDistributionList;
|
||||
private final Optional<SignalContactRecord> contact;
|
||||
private final Optional<SignalGroupV1Record> groupV1;
|
||||
private final Optional<SignalGroupV2Record> groupV2;
|
||||
private final Optional<SignalAccountRecord> account;
|
||||
|
||||
public static SignalStorageRecord forStoryDistributionList(SignalStoryDistributionListRecord storyDistributionList) {
|
||||
return forStoryDistributionList(storyDistributionList.getId(), storyDistributionList);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forStoryDistributionList(StorageId key, SignalStoryDistributionListRecord storyDistributionList) {
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(storyDistributionList));
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
||||
return forContact(contact.getId(), contact);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) {
|
||||
return new SignalStorageRecord(key, Optional.of(contact), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
return new SignalStorageRecord(key, Optional.of(contact), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
||||
|
@ -25,7 +34,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.of(groupV1), Optional.empty(), Optional.empty());
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.of(groupV1), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) {
|
||||
|
@ -33,7 +42,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) {
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.of(groupV2), Optional.empty());
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.of(groupV2), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forAccount(SignalAccountRecord account) {
|
||||
|
@ -41,24 +50,26 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
}
|
||||
|
||||
public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) {
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(account));
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(account), Optional.empty());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forUnknown(StorageId key) {
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
return new SignalStorageRecord(key, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
private SignalStorageRecord(StorageId id,
|
||||
Optional<SignalContactRecord> contact,
|
||||
Optional<SignalGroupV1Record> groupV1,
|
||||
Optional<SignalGroupV2Record> groupV2,
|
||||
Optional<SignalAccountRecord> account)
|
||||
Optional<SignalAccountRecord> account,
|
||||
Optional<SignalStoryDistributionListRecord> storyDistributionList)
|
||||
{
|
||||
this.id = id;
|
||||
this.contact = contact;
|
||||
this.groupV1 = groupV1;
|
||||
this.groupV2 = groupV2;
|
||||
this.account = account;
|
||||
this.id = id;
|
||||
this.contact = contact;
|
||||
this.groupV1 = groupV1;
|
||||
this.groupV2 = groupV2;
|
||||
this.account = account;
|
||||
this.storyDistributionList = storyDistributionList;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -96,8 +107,12 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
return account;
|
||||
}
|
||||
|
||||
public Optional<SignalStoryDistributionListRecord> getStoryDistributionList() {
|
||||
return storyDistributionList;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent();
|
||||
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent() && !storyDistributionList.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,11 +123,12 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
return Objects.equals(id, that.id) &&
|
||||
Objects.equals(contact, that.contact) &&
|
||||
Objects.equals(groupV1, that.groupV1) &&
|
||||
Objects.equals(groupV2, that.groupV2);
|
||||
Objects.equals(groupV2, that.groupV2) &&
|
||||
Objects.equals(storyDistributionList, that.storyDistributionList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, contact, groupV1, groupV2);
|
||||
return Objects.hash(id, contact, groupV1, groupV2, storyDistributionList);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.ProtoUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SignalStoryDistributionListRecord implements SignalRecord {
|
||||
|
||||
private static final String TAG = SignalStoryDistributionListRecord.class.getSimpleName();
|
||||
|
||||
private final StorageId id;
|
||||
private final StoryDistributionListRecord proto;
|
||||
private final boolean hasUnknownFields;
|
||||
private final List<SignalServiceAddress> recipients;
|
||||
|
||||
public SignalStoryDistributionListRecord(StorageId id, StoryDistributionListRecord proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||
this.recipients = proto.getRecipientUuidsList()
|
||||
.stream()
|
||||
.map(ServiceId::parseOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalStorageRecord asStorageRecord() {
|
||||
return SignalStorageRecord.forStoryDistributionList(this);
|
||||
}
|
||||
|
||||
public StoryDistributionListRecord toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public byte[] serializeUnknownFields() {
|
||||
return hasUnknownFields ? proto.toByteArray() : null;
|
||||
}
|
||||
|
||||
public byte[] getIdentifier() {
|
||||
return proto.getIdentifier().toByteArray();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return proto.getName();
|
||||
}
|
||||
|
||||
public List<SignalServiceAddress> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public long getDeletedAtTimestamp() {
|
||||
return proto.getDeletedAtTimestamp();
|
||||
}
|
||||
|
||||
public boolean allowsReplies() {
|
||||
return proto.getAllowsReplies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describeDiff(SignalRecord other) {
|
||||
if (other instanceof SignalStoryDistributionListRecord) {
|
||||
SignalStoryDistributionListRecord that = (SignalStoryDistributionListRecord) other;
|
||||
List<String> diff = new LinkedList<>();
|
||||
|
||||
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
|
||||
diff.add("ID");
|
||||
}
|
||||
|
||||
if (!Arrays.equals(this.getIdentifier(), that.getIdentifier())) {
|
||||
diff.add("Identifier");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.getName(), that.getName())) {
|
||||
diff.add("Name");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.recipients, that.recipients)) {
|
||||
diff.add("RecipientUuids");
|
||||
}
|
||||
|
||||
if (this.getDeletedAtTimestamp() != that.getDeletedAtTimestamp()) {
|
||||
diff.add("DeletedAtTimestamp");
|
||||
}
|
||||
|
||||
if (this.allowsReplies() != that.allowsReplies()) {
|
||||
diff.add("AllowsReplies");
|
||||
}
|
||||
|
||||
return diff.toString();
|
||||
} else {
|
||||
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalStoryDistributionListRecord that = (SignalStoryDistributionListRecord) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final StoryDistributionListRecord.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, byte[] serializedUnknowns) {
|
||||
this.id = StorageId.forStoryDistributionList(rawId);
|
||||
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns);
|
||||
} else {
|
||||
this.builder = StoryDistributionListRecord.newBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setIdentifier(byte[] identifier) {
|
||||
builder.setIdentifier(ByteString.copyFrom(identifier));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setName(String name) {
|
||||
builder.setName(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecipients(List<SignalServiceAddress> recipients) {
|
||||
builder.clearRecipientUuids();
|
||||
builder.addAllRecipientUuids(recipients.stream()
|
||||
.map(SignalServiceAddress::getIdentifier)
|
||||
.collect(Collectors.toList()));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDeletedAtTimestamp(long deletedAtTimestamp) {
|
||||
builder.setDeletedAtTimestamp(deletedAtTimestamp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowsReplies(boolean allowsReplies) {
|
||||
builder.setAllowsReplies(allowsReplies);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalStoryDistributionListRecord build() {
|
||||
return new SignalStoryDistributionListRecord(id, builder.build());
|
||||
}
|
||||
|
||||
private static StoryDistributionListRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
|
||||
try {
|
||||
return StoryDistributionListRecord.parseFrom(serializedUnknowns).toBuilder();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e);
|
||||
return StoryDistributionListRecord.newBuilder();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,10 @@ public class StorageId {
|
|||
return new StorageId(ManifestRecord.Identifier.Type.GROUPV2_VALUE, Preconditions.checkNotNull(raw));
|
||||
}
|
||||
|
||||
public static StorageId forStoryDistributionList(byte[] raw) {
|
||||
return new StorageId(ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST_VALUE, Preconditions.checkNotNull(raw));
|
||||
}
|
||||
|
||||
public static StorageId forAccount(byte[] raw) {
|
||||
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT_VALUE, Preconditions.checkNotNull(raw));
|
||||
}
|
||||
|
|
|
@ -38,11 +38,12 @@ message WriteOperation {
|
|||
message ManifestRecord {
|
||||
message Identifier {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CONTACT = 1;
|
||||
GROUPV1 = 2;
|
||||
GROUPV2 = 3;
|
||||
ACCOUNT = 4;
|
||||
UNKNOWN = 0;
|
||||
CONTACT = 1;
|
||||
GROUPV1 = 2;
|
||||
GROUPV2 = 3;
|
||||
ACCOUNT = 4;
|
||||
STORY_DISTRIBUTION_LIST = 5;
|
||||
}
|
||||
|
||||
bytes raw = 1;
|
||||
|
@ -55,10 +56,11 @@ message ManifestRecord {
|
|||
|
||||
message StorageRecord {
|
||||
oneof record {
|
||||
ContactRecord contact = 1;
|
||||
GroupV1Record groupV1 = 2;
|
||||
GroupV2Record groupV2 = 3;
|
||||
AccountRecord account = 4;
|
||||
ContactRecord contact = 1;
|
||||
GroupV1Record groupV1 = 2;
|
||||
GroupV2Record groupV2 = 3;
|
||||
AccountRecord account = 4;
|
||||
StoryDistributionListRecord storyDistributionList = 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,3 +158,11 @@ message AccountRecord {
|
|||
bool displayBadgesOnProfile = 23;
|
||||
bool subscriptionManuallyCancelled = 24;
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
bytes identifier = 1;
|
||||
string name = 2;
|
||||
repeated string recipientUuids = 3;
|
||||
uint64 deletedAtTimestamp = 4;
|
||||
bool allowsReplies = 5;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue