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 androidx.core.content.contentValuesOf
|
||||||
import org.signal.core.util.CursorUtil
|
import org.signal.core.util.CursorUtil
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireString
|
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.DistributionListRecord
|
||||||
import org.thoughtcrime.securesms.database.model.StoryType
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.util.Base64
|
import org.thoughtcrime.securesms.util.Base64
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,6 +29,8 @@ import java.util.UUID
|
||||||
class DistributionListDatabase constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
|
class DistributionListDatabase constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val TAG = Log.tag(DistributionListDatabase::class.java)
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
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(
|
val recipientId = db.insert(
|
||||||
RecipientDatabase.TABLE_NAME, null,
|
RecipientDatabase.TABLE_NAME, null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
|
RecipientDatabase.GROUP_TYPE to RecipientDatabase.GroupType.DISTRIBUTION_LIST.id,
|
||||||
RecipientDatabase.DISTRIBUTION_LIST_ID to DistributionListId.MY_STORY_ID,
|
RecipientDatabase.DISTRIBUTION_LIST_ID to DistributionListId.MY_STORY_ID,
|
||||||
RecipientDatabase.STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
RecipientDatabase.STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||||
RecipientDatabase.PROFILE_SHARING to 1
|
RecipientDatabase.PROFILE_SHARING to 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val listUUID = UUID.randomUUID().toString()
|
|
||||||
db.insert(
|
db.insert(
|
||||||
ListTable.TABLE_NAME, null,
|
ListTable.TABLE_NAME, null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
ListTable.ID to DistributionListId.MY_STORY_ID,
|
ListTable.ID to DistributionListId.MY_STORY_ID,
|
||||||
ListTable.NAME to listUUID,
|
ListTable.NAME to DistributionId.MY_STORY.toString(),
|
||||||
ListTable.DISTRIBUTION_ID to listUUID,
|
ListTable.DISTRIBUTION_ID to DistributionId.MY_STORY.toString(),
|
||||||
ListTable.RECIPIENT_ID to recipientId
|
ListTable.RECIPIENT_ID to recipientId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -60,6 +66,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
const val DISTRIBUTION_ID = "distribution_id"
|
const val DISTRIBUTION_ID = "distribution_id"
|
||||||
const val RECIPIENT_ID = "recipient_id"
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
const val ALLOWS_REPLIES = "allows_replies"
|
const val ALLOWS_REPLIES = "allows_replies"
|
||||||
|
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||||
|
|
||||||
const val CREATE_TABLE = """
|
const val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
@ -67,9 +74,12 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
$NAME TEXT UNIQUE NOT NULL,
|
$NAME TEXT UNIQUE NOT NULL,
|
||||||
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
||||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
$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 {
|
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 projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
|
||||||
|
|
||||||
val where = when {
|
val where = when {
|
||||||
query.isNullOrEmpty() && includeMyStory -> null
|
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
|
||||||
query.isNullOrEmpty() -> "${ListTable.ID} != ?"
|
query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
|
||||||
includeMyStory -> "${ListTable.NAME} LIKE ? OR ${ListTable.ID} == ?"
|
includeMyStory -> "(${ListTable.NAME} LIKE ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED}"
|
||||||
else -> "${ListTable.NAME} LIKE ? AND ${ListTable.ID} != ?"
|
else -> "${ListTable.NAME} LIKE ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val whereArgs = when {
|
val whereArgs = when {
|
||||||
|
@ -145,7 +155,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||||
val db = readableDatabase
|
val db = readableDatabase
|
||||||
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
|
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 {
|
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
|
||||||
val results = mutableListOf<DistributionListPartialRecord>()
|
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.
|
* @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
|
val db = writableDatabase
|
||||||
|
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
try {
|
try {
|
||||||
val values = ContentValues().apply {
|
val values = ContentValues().apply {
|
||||||
put(ListTable.NAME, name)
|
put(ListTable.NAME, if (deletionTimestamp == 0L) name else createUniqueNameForDeletedStory())
|
||||||
put(ListTable.DISTRIBUTION_ID, UUID.randomUUID().toString())
|
put(ListTable.DISTRIBUTION_ID, distributionId.toString())
|
||||||
|
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
|
||||||
putNull(ListTable.RECIPIENT_ID)
|
putNull(ListTable.RECIPIENT_ID)
|
||||||
|
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
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 {
|
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()) {
|
return if (cursor.moveToFirst()) {
|
||||||
if (CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES)) {
|
if (CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES)) {
|
||||||
StoryType.STORY_WITH_REPLIES
|
StoryType.STORY_WITH_REPLIES
|
||||||
|
@ -217,10 +235,29 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAllowsReplies(listId: DistributionListId, allowsReplies: Boolean) {
|
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? {
|
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 ->
|
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||||
return if (cursor.moveToFirst()) {
|
return if (cursor.moveToFirst()) {
|
||||||
val id: DistributionListId = DistributionListId.from(cursor.requireLong(ListTable.ID))
|
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),
|
name = cursor.requireNonNullString(ListTable.NAME),
|
||||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||||
members = getMembers(id)
|
members = getRawMembers(id),
|
||||||
|
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -239,7 +277,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDistributionId(listId: DistributionListId): DistributionId? {
|
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()) {
|
return if (cursor.moveToFirst()) {
|
||||||
DistributionId.from(cursor.requireString(ListTable.DISTRIBUTION_ID))
|
DistributionId.from(cursor.requireString(ListTable.DISTRIBUTION_ID))
|
||||||
} else {
|
} else {
|
||||||
|
@ -318,7 +356,130 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
writableDatabase.update(MembershipTable.TABLE_NAME, values, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(oldId))
|
writableDatabase.update(MembershipTable.TABLE_NAME, values, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(oldId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteList(distributionListId: DistributionListId) {
|
fun deleteList(distributionListId: DistributionListId, deletionTimestamp: Long = System.currentTimeMillis()) {
|
||||||
writableDatabase.delete(ListTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(distributionListId))
|
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,
|
DISTRIBUTION_LIST_ID,
|
||||||
distributionListId.serialize(),
|
distributionListId.serialize(),
|
||||||
ContentValues().apply {
|
ContentValues().apply {
|
||||||
|
put(GROUP_TYPE, GroupType.DISTRIBUTION_LIST.id)
|
||||||
put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
|
put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
|
||||||
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
|
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
|
||||||
put(PROFILE_SHARING, 1)
|
put(PROFILE_SHARING, 1)
|
||||||
|
@ -1071,10 +1072,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
$STORAGE_SERVICE_ID NOT NULL AND (
|
$STORAGE_SERVICE_ID NOT NULL AND (
|
||||||
($GROUP_TYPE = ? AND $SERVICE_ID NOT NULL AND $ID != ?)
|
($GROUP_TYPE = ? AND $SERVICE_ID NOT NULL AND $ID != ?)
|
||||||
OR
|
OR
|
||||||
$GROUP_TYPE IN (?)
|
$GROUP_TYPE IN (?, ?)
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".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()
|
val out: MutableMap<RecipientId, StorageId> = HashMap()
|
||||||
|
|
||||||
readableDatabase.query(TABLE_NAME, arrayOf(ID, STORAGE_SERVICE_ID, GROUP_TYPE), query, args, null, null, null).use { cursor ->
|
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) {
|
when (groupType) {
|
||||||
GroupType.NONE -> out[id] = StorageId.forContact(key)
|
GroupType.NONE -> out[id] = StorageId.forContact(key)
|
||||||
GroupType.SIGNAL_V1 -> out[id] = StorageId.forGroupV1(key)
|
GroupType.SIGNAL_V1 -> out[id] = StorageId.forGroupV1(key)
|
||||||
|
GroupType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2504,7 +2506,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?, ?) OR $REGISTERED = ?)"
|
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)
|
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.conversation.colors.ChatColorsMapper.entrySet
|
||||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
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.database.model.databaseprotos.ReactionList
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
|
@ -196,8 +195,9 @@ object SignalDatabaseMigrations {
|
||||||
private const val GROUP_STORIES = 134
|
private const val GROUP_STORIES = 134
|
||||||
private const val MMS_COUNT_INDEX = 135
|
private const val MMS_COUNT_INDEX = 135
|
||||||
private const val STORY_SENDS = 136
|
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
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -2458,7 +2458,7 @@ object SignalDatabaseMigrations {
|
||||||
val recipientId = db.insert(
|
val recipientId = db.insert(
|
||||||
"recipient", null,
|
"recipient", null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
"distribution_list_id" to DistributionListId.MY_STORY_ID,
|
"distribution_list_id" to 1L,
|
||||||
"storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
"storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||||
"profile_sharing" to 1
|
"profile_sharing" to 1
|
||||||
)
|
)
|
||||||
|
@ -2468,7 +2468,7 @@ object SignalDatabaseMigrations {
|
||||||
db.insert(
|
db.insert(
|
||||||
"distribution_list", null,
|
"distribution_list", null,
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
"_id" to DistributionListId.MY_STORY_ID,
|
"_id" to 1L,
|
||||||
"name" to listUUID,
|
"name" to listUUID,
|
||||||
"distribution_id" to listUUID,
|
"distribution_id" to listUUID,
|
||||||
"recipient_id" to recipientId
|
"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)")
|
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
|
@JvmStatic
|
||||||
|
|
|
@ -11,5 +11,6 @@ data class DistributionListRecord(
|
||||||
val name: String,
|
val name: String,
|
||||||
val distributionId: DistributionId,
|
val distributionId: DistributionId,
|
||||||
val allowsReplies: Boolean,
|
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.StorageSyncHelper.WriteOperationResult;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||||
|
import org.thoughtcrime.securesms.storage.StoryDistributionListRecordProcessor;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
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.SignalRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
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.StorageId;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -274,6 +277,7 @@ public class StorageSyncJob extends BaseJob {
|
||||||
List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
|
List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
|
||||||
List<SignalAccountRecord> remoteAccount = new LinkedList<>();
|
List<SignalAccountRecord> remoteAccount = new LinkedList<>();
|
||||||
List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
|
List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
|
||||||
|
List<SignalStoryDistributionListRecord> remoteStoryDistributionLists = new LinkedList<>();
|
||||||
|
|
||||||
for (SignalStorageRecord remote : remoteOnly) {
|
for (SignalStorageRecord remote : remoteOnly) {
|
||||||
if (remote.getContact().isPresent()) {
|
if (remote.getContact().isPresent()) {
|
||||||
|
@ -284,6 +288,8 @@ public class StorageSyncJob extends BaseJob {
|
||||||
remoteGv2.add(remote.getGroupV2().get());
|
remoteGv2.add(remote.getGroupV2().get());
|
||||||
} else if (remote.getAccount().isPresent()) {
|
} else if (remote.getAccount().isPresent()) {
|
||||||
remoteAccount.add(remote.getAccount().get());
|
remoteAccount.add(remote.getAccount().get());
|
||||||
|
} else if (remote.getStoryDistributionList().isPresent()) {
|
||||||
|
remoteStoryDistributionLists.add(remote.getStoryDistributionList().get());
|
||||||
} else if (remote.getId().isUnknown()) {
|
} else if (remote.getId().isUnknown()) {
|
||||||
remoteUnknown.add(remote);
|
remoteUnknown.add(remote);
|
||||||
} else {
|
} else {
|
||||||
|
@ -302,6 +308,7 @@ public class StorageSyncJob extends BaseJob {
|
||||||
new GroupV2RecordProcessor(context).process(remoteGv2, StorageSyncHelper.KEY_GENERATOR);
|
new GroupV2RecordProcessor(context).process(remoteGv2, StorageSyncHelper.KEY_GENERATOR);
|
||||||
self = freshSelf();
|
self = freshSelf();
|
||||||
new AccountRecordProcessor(context, self).process(remoteAccount, StorageSyncHelper.KEY_GENERATOR);
|
new AccountRecordProcessor(context, self).process(remoteAccount, StorageSyncHelper.KEY_GENERATOR);
|
||||||
|
new StoryDistributionListRecordProcessor().process(remoteStoryDistributionLists, StorageSyncHelper.KEY_GENERATOR);
|
||||||
|
|
||||||
List<SignalStorageRecord> unknownInserts = remoteUnknown;
|
List<SignalStorageRecord> unknownInserts = remoteUnknown;
|
||||||
List<StorageId> unknownDeletes = Stream.of(idDifference.getLocalOnlyIds()).filter(StorageId::isUnknown).toList();
|
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));
|
records.add(StorageSyncHelper.buildAccountRecord(context, self));
|
||||||
break;
|
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:
|
default:
|
||||||
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
|
||||||
if (unknown != null) {
|
if (unknown != null) {
|
||||||
|
|
|
@ -100,9 +100,10 @@ public class ApplicationMigrations {
|
||||||
static final int PNI_IDENTITY = 56;
|
static final int PNI_IDENTITY = 56;
|
||||||
static final int PNI_IDENTITY_2 = 57;
|
static final int PNI_IDENTITY_2 = 57;
|
||||||
static final int PNI_IDENTITY_3 = 58;
|
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
|
* 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());
|
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;
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,19 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
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.database.model.RecipientRecord;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.subscription.Subscriber;
|
import org.thoughtcrime.securesms.subscription.Subscriber;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
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.SignalGroupV1Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
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.subscriptions.SubscriberId;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class StorageSyncModels {
|
public final class StorageSyncModels {
|
||||||
|
|
||||||
|
@ -50,6 +59,7 @@ public final class StorageSyncModels {
|
||||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId));
|
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId));
|
||||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||||
case SIGNAL_V2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, settings.getSyncExtras().getGroupMasterKey()));
|
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!");
|
default: throw new AssertionError("Unsupported type!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,6 +171,38 @@ public final class StorageSyncModels {
|
||||||
.build();
|
.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) {
|
public static @NonNull IdentityDatabase.VerifiedStatus remoteToLocalIdentityStatus(@NonNull IdentityState identityState) {
|
||||||
switch (identityState) {
|
switch (identityState) {
|
||||||
case VERIFIED: return IdentityDatabase.VerifiedStatus.VERIFIED;
|
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.R
|
||||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
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.RecipientId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
|
||||||
|
@ -51,4 +53,16 @@ object Stories {
|
||||||
|
|
||||||
return RecipientUtil.getEligibleForSending(recipientIds.map(Recipient::resolved))
|
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 io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
|
||||||
class CreateStoryWithViewersRepository {
|
class CreateStoryWithViewersRepository {
|
||||||
fun createList(name: CharSequence, members: Set<RecipientId>): Single<RecipientId> {
|
fun createList(name: CharSequence, members: Set<RecipientId>): Single<RecipientId> {
|
||||||
|
@ -12,6 +13,7 @@ class CreateStoryWithViewersRepository {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
it.onError(Exception("Null result, due to a duplicated name."))
|
it.onError(Exception("Null result, due to a duplicated name."))
|
||||||
} else {
|
} else {
|
||||||
|
Stories.onStorySettingsChanged(result)
|
||||||
it.onSuccess(SignalDatabase.recipients.getOrInsertFromDistributionListId(result))
|
it.onSuccess(SignalDatabase.recipients.getOrInsertFromDistributionListId(result))
|
||||||
}
|
}
|
||||||
}.subscribeOn(Schedulers.io())
|
}.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.DistributionListId
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
|
||||||
class PrivateStorySettingsRepository {
|
class PrivateStorySettingsRepository {
|
||||||
fun getRecord(distributionListId: DistributionListId): Single<DistributionListRecord> {
|
fun getRecord(distributionListId: DistributionListId): Single<DistributionListRecord> {
|
||||||
|
@ -18,12 +19,14 @@ class PrivateStorySettingsRepository {
|
||||||
fun removeMember(distributionListId: DistributionListId, member: RecipientId): Completable {
|
fun removeMember(distributionListId: DistributionListId, member: RecipientId): Completable {
|
||||||
return Completable.fromAction {
|
return Completable.fromAction {
|
||||||
SignalDatabase.distributionLists.removeMemberFromList(distributionListId, member)
|
SignalDatabase.distributionLists.removeMemberFromList(distributionListId, member)
|
||||||
|
Stories.onStorySettingsChanged(distributionListId)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(distributionListId: DistributionListId): Completable {
|
fun delete(distributionListId: DistributionListId): Completable {
|
||||||
return Completable.fromAction {
|
return Completable.fromAction {
|
||||||
SignalDatabase.distributionLists.deleteList(distributionListId)
|
SignalDatabase.distributionLists.deleteList(distributionListId)
|
||||||
|
Stories.onStorySettingsChanged(distributionListId)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ class PrivateStorySettingsRepository {
|
||||||
fun setRepliesAndReactionsEnabled(distributionListId: DistributionListId, repliesAndReactionsEnabled: Boolean): Completable {
|
fun setRepliesAndReactionsEnabled(distributionListId: DistributionListId, repliesAndReactionsEnabled: Boolean): Completable {
|
||||||
return Completable.fromAction {
|
return Completable.fromAction {
|
||||||
SignalDatabase.distributionLists.setAllowsReplies(distributionListId, repliesAndReactionsEnabled)
|
SignalDatabase.distributionLists.setAllowsReplies(distributionListId, repliesAndReactionsEnabled)
|
||||||
|
Stories.onStorySettingsChanged(distributionListId)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
|
||||||
class EditStoryNameRepository {
|
class EditStoryNameRepository {
|
||||||
fun save(privateStoryId: DistributionListId, name: CharSequence): Completable {
|
fun save(privateStoryId: DistributionListId, name: CharSequence): Completable {
|
||||||
|
@ -13,6 +14,8 @@ class EditStoryNameRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SignalDatabase.distributionLists.setName(privateStoryId, name.toString())) {
|
if (SignalDatabase.distributionLists.setName(privateStoryId, name.toString())) {
|
||||||
|
Stories.onStorySettingsChanged(privateStoryId)
|
||||||
|
|
||||||
it.onComplete()
|
it.onComplete()
|
||||||
} else {
|
} else {
|
||||||
it.onError(Exception("Could not update story name."))
|
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 io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
|
||||||
class MyStorySettingsRepository {
|
class MyStorySettingsRepository {
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ class MyStorySettingsRepository {
|
||||||
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean): Completable {
|
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean): Completable {
|
||||||
return Completable.fromAction {
|
return Completable.fromAction {
|
||||||
SignalDatabase.distributionLists.setAllowsReplies(DistributionListId.MY_STORY, repliesAndReactionsEnabled)
|
SignalDatabase.distributionLists.setAllowsReplies(DistributionListId.MY_STORY, repliesAndReactionsEnabled)
|
||||||
|
Stories.onStorySettingsChanged(DistributionListId.MY_STORY)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
|
||||||
class BaseStoryRecipientSelectionRepository {
|
class BaseStoryRecipientSelectionRepository {
|
||||||
fun updateDistributionListMembership(distributionListId: DistributionListId, recipients: Set<RecipientId>) {
|
fun updateDistributionListMembership(distributionListId: DistributionListId, recipients: Set<RecipientId>) {
|
||||||
|
@ -23,6 +24,8 @@ class BaseStoryRecipientSelectionRepository {
|
||||||
newNotOld.forEach {
|
newNotOld.forEach {
|
||||||
SignalDatabase.distributionLists.addMemberToList(distributionListId, it)
|
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 final class DistributionId {
|
||||||
|
|
||||||
|
public static final DistributionId MY_STORY = DistributionId.from("00000000-0000-0000-0000-000000000000");
|
||||||
|
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
|
|
||||||
public static DistributionId from(String id) {
|
public static DistributionId from(String id) {
|
||||||
|
|
|
@ -44,6 +44,8 @@ public final class SignalStorageModels {
|
||||||
return SignalStorageRecord.forGroupV2(id, new SignalGroupV2Record(id, record.getGroupV2()));
|
return SignalStorageRecord.forGroupV2(id, new SignalGroupV2Record(id, record.getGroupV2()));
|
||||||
} else if (record.hasAccount() && type == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) {
|
} else if (record.hasAccount() && type == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) {
|
||||||
return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.getAccount()));
|
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 {
|
} else {
|
||||||
if (StorageId.isKnownType(type)) {
|
if (StorageId.isKnownType(type)) {
|
||||||
Log.w(TAG, "StorageId is of known type (" + type + "), but the data is bad! Falling back to unknown.");
|
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());
|
builder.setGroupV2(record.getGroupV2().get().toProto());
|
||||||
} else if (record.getAccount().isPresent()) {
|
} else if (record.getAccount().isPresent()) {
|
||||||
builder.setAccount(record.getAccount().get().toProto());
|
builder.setAccount(record.getAccount().get().toProto());
|
||||||
|
} else if (record.getStoryDistributionList().isPresent()) {
|
||||||
|
builder.setStoryDistributionList(record.getStoryDistributionList().get().toProto());
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidStorageWriteError();
|
throw new InvalidStorageWriteError();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,17 +7,26 @@ import java.util.Optional;
|
||||||
public class SignalStorageRecord implements SignalRecord {
|
public class SignalStorageRecord implements SignalRecord {
|
||||||
|
|
||||||
private final StorageId id;
|
private final StorageId id;
|
||||||
|
private final Optional<SignalStoryDistributionListRecord> storyDistributionList;
|
||||||
private final Optional<SignalContactRecord> contact;
|
private final Optional<SignalContactRecord> contact;
|
||||||
private final Optional<SignalGroupV1Record> groupV1;
|
private final Optional<SignalGroupV1Record> groupV1;
|
||||||
private final Optional<SignalGroupV2Record> groupV2;
|
private final Optional<SignalGroupV2Record> groupV2;
|
||||||
private final Optional<SignalAccountRecord> account;
|
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) {
|
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
||||||
return forContact(contact.getId(), contact);
|
return forContact(contact.getId(), contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord 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) {
|
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
||||||
|
@ -25,7 +34,7 @@ public class SignalStorageRecord implements SignalRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
|
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) {
|
public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) {
|
||||||
|
@ -33,7 +42,7 @@ public class SignalStorageRecord implements SignalRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) {
|
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) {
|
public static SignalStorageRecord forAccount(SignalAccountRecord account) {
|
||||||
|
@ -41,24 +50,26 @@ public class SignalStorageRecord implements SignalRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) {
|
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) {
|
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,
|
private SignalStorageRecord(StorageId id,
|
||||||
Optional<SignalContactRecord> contact,
|
Optional<SignalContactRecord> contact,
|
||||||
Optional<SignalGroupV1Record> groupV1,
|
Optional<SignalGroupV1Record> groupV1,
|
||||||
Optional<SignalGroupV2Record> groupV2,
|
Optional<SignalGroupV2Record> groupV2,
|
||||||
Optional<SignalAccountRecord> account)
|
Optional<SignalAccountRecord> account,
|
||||||
|
Optional<SignalStoryDistributionListRecord> storyDistributionList)
|
||||||
{
|
{
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.contact = contact;
|
this.contact = contact;
|
||||||
this.groupV1 = groupV1;
|
this.groupV1 = groupV1;
|
||||||
this.groupV2 = groupV2;
|
this.groupV2 = groupV2;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
|
this.storyDistributionList = storyDistributionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -96,8 +107,12 @@ public class SignalStorageRecord implements SignalRecord {
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<SignalStoryDistributionListRecord> getStoryDistributionList() {
|
||||||
|
return storyDistributionList;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUnknown() {
|
public boolean isUnknown() {
|
||||||
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent();
|
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent() && !storyDistributionList.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -108,11 +123,12 @@ public class SignalStorageRecord implements SignalRecord {
|
||||||
return Objects.equals(id, that.id) &&
|
return Objects.equals(id, that.id) &&
|
||||||
Objects.equals(contact, that.contact) &&
|
Objects.equals(contact, that.contact) &&
|
||||||
Objects.equals(groupV1, that.groupV1) &&
|
Objects.equals(groupV1, that.groupV1) &&
|
||||||
Objects.equals(groupV2, that.groupV2);
|
Objects.equals(groupV2, that.groupV2) &&
|
||||||
|
Objects.equals(storyDistributionList, that.storyDistributionList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
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));
|
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) {
|
public static StorageId forAccount(byte[] raw) {
|
||||||
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT_VALUE, Preconditions.checkNotNull(raw));
|
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT_VALUE, Preconditions.checkNotNull(raw));
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ message ManifestRecord {
|
||||||
GROUPV1 = 2;
|
GROUPV1 = 2;
|
||||||
GROUPV2 = 3;
|
GROUPV2 = 3;
|
||||||
ACCOUNT = 4;
|
ACCOUNT = 4;
|
||||||
|
STORY_DISTRIBUTION_LIST = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes raw = 1;
|
bytes raw = 1;
|
||||||
|
@ -59,6 +60,7 @@ message StorageRecord {
|
||||||
GroupV1Record groupV1 = 2;
|
GroupV1Record groupV1 = 2;
|
||||||
GroupV2Record groupV2 = 3;
|
GroupV2Record groupV2 = 3;
|
||||||
AccountRecord account = 4;
|
AccountRecord account = 4;
|
||||||
|
StoryDistributionListRecord storyDistributionList = 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,3 +158,11 @@ message AccountRecord {
|
||||||
bool displayBadgesOnProfile = 23;
|
bool displayBadgesOnProfile = 23;
|
||||||
bool subscriptionManuallyCancelled = 24;
|
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