Implement StoryDistributionListRecord and processing.

fork-5.53.8
Alex Hart 2022-03-25 14:27:03 -03:00 zatwierdzone przez Cody Henthorne
rodzic 2cd7462573
commit c359b0134a
21 zmienionych plików z 896 dodań i 64 usunięć

Wyświetl plik

@ -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()}"
}
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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())

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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."))

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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();
}
}
}
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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;
}