From 89767cc2608fff27f29916f6d5de55ccfbe6d957 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 7 Nov 2024 13:31:06 -0500 Subject: [PATCH] Convert StoryDistributionListRecordProcessor to kotlin. --- .../StoryDistributionListRecordProcessor.java | 189 ----------------- .../StoryDistributionListRecordProcessor.kt | 194 ++++++++++++++++++ 2 files changed, 194 insertions(+), 189 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java deleted file mode 100644 index 6c191ec6c6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java +++ /dev/null @@ -1,189 +0,0 @@ -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.RecipientTable; -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.Collection; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; - -public class StoryDistributionListRecordProcessor extends DefaultStorageRecordProcessor { - - private static final String TAG = Log.tag(StoryDistributionListRecordProcessor.class); - - private boolean haveSeenMyStory; - - /** - * At a minimum, we require: - * - */ - @Override - public 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 - public @NonNull Optional getMatching(@NonNull SignalStoryDistributionListRecord remote, @NonNull StorageKeyGenerator keyGenerator) { - Log.d(TAG, "Attempting to get matching record..."); - RecipientId matching = SignalDatabase.distributionLists().getRecipientIdForSyncRecord(remote); - if (matching == null && UuidUtil.parseOrThrow(remote.getIdentifier()).equals(DistributionId.MY_STORY.asUuid())) { - Log.e(TAG, "Cannot find matching database record for My Story."); - throw new MyStoryDoesNotExistException(); - } - - if (matching != null) { - Log.d(TAG, "Found a matching RecipientId for the distribution list..."); - RecipientRecord recordForSync = SignalDatabase.recipients().getRecordForSync(matching); - if (recordForSync == null) { - Log.e(TAG, "Could not find a record for the recipient id in the recipient table"); - throw new IllegalStateException("Found matching recipient but couldn't generate record for sync."); - } - - if (recordForSync.getRecipientType().getId() != RecipientTable.RecipientType.DISTRIBUTION_LIST.getId()) { - Log.d(TAG, "Record has an incorrect group type."); - throw new InvalidGroupTypeException(); - } - - Optional record = StorageSyncModels.localToRemoteRecord(recordForSync).getStoryDistributionList(); - if (record.isPresent()) { - Log.d(TAG, "Found a matching record."); - return record; - } else { - Log.e(TAG, "Could not resolve the record"); - throw new UnexpectedEmptyOptionalException(); - } - } else { - Log.d(TAG, "Could not find a matching record. Returning an empty."); - return Optional.empty(); - } - } - - @Override - public @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 recipients = remote.getRecipients(); - long deletedAtTimestamp = remote.getDeletedAtTimestamp(); - boolean allowsReplies = remote.allowsReplies(); - boolean isBlockList = remote.isBlockList(); - - boolean matchesRemote = doParamsMatch(remote, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies, isBlockList); - boolean matchesLocal = doParamsMatch(local, unknownFields, identifier, name, recipients, deletedAtTimestamp, allowsReplies, isBlockList); - - 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) - .setIsBlockList(isBlockList) - .build(); - } - } - - @Override - public void insertLocal(@NonNull SignalStoryDistributionListRecord record) throws IOException { - SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListInsert(record); - } - - @Override - public void updateLocal(@NonNull StorageRecordUpdate 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 recipients, - long deletedAtTimestamp, - boolean allowsReplies, - boolean isBlockList) { - 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() && - isBlockList == record.isBlockList(); - } - - /** - * Thrown when the RecipientSettings object for a given distribution list is not the - * correct group type (4). - */ - private static class InvalidGroupTypeException extends RuntimeException {} - - /** - * Thrown when the distribution list object returned from the storage sync helper is - * absent, even though a RecipientSettings was found. - */ - private static class UnexpectedEmptyOptionalException extends RuntimeException {} - - /** - * Thrown when we try to ge the matching record for the "My Story" distribution ID but - * it isn't in the database. - */ - private static class MyStoryDoesNotExistException extends RuntimeException {} -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.kt new file mode 100644 index 0000000000..ebc38a369f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.kt @@ -0,0 +1,194 @@ +package org.thoughtcrime.securesms.storage + +import org.signal.core.util.StringUtil +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.RecipientTable +import org.thoughtcrime.securesms.database.SignalDatabase +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.Optional + +class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor() { + + companion object { + private val TAG = Log.tag(StoryDistributionListRecordProcessor::class.java) + } + + private var haveSeenMyStory = false + + /** + * At a minimum, we require: + * + * - A valid identifier + * - A non-visually-empty name field OR a deleted at timestamp + */ + override fun isInvalid(remote: SignalStoryDistributionListRecord): Boolean { + val remoteUuid = UuidUtil.parseOrNull(remote.identifier) + if (remoteUuid == null) { + Log.d(TAG, "Bad distribution list identifier -- marking as invalid") + return true + } + + val isMyStory = remoteUuid == DistributionId.MY_STORY.asUuid() + if (haveSeenMyStory && isMyStory) { + Log.w(TAG, "Found an additional MyStory record -- marking as invalid") + return true + } + + haveSeenMyStory = haveSeenMyStory or isMyStory + + if (remote.deletedAtTimestamp > 0L) { + if (isMyStory) { + Log.w(TAG, "Refusing to delete My Story -- marking as invalid") + return true + } else { + return false + } + } + + if (StringUtil.isVisuallyEmpty(remote.name)) { + Log.d(TAG, "Bad distribution list name (visually empty) -- marking as invalid") + return true + } + + return false + } + + override fun getMatching(remote: SignalStoryDistributionListRecord, keyGenerator: StorageKeyGenerator): Optional { + Log.d(TAG, "Attempting to get matching record...") + val matching = SignalDatabase.distributionLists.getRecipientIdForSyncRecord(remote) + if (matching == null && UuidUtil.parseOrThrow(remote.identifier) == DistributionId.MY_STORY.asUuid()) { + Log.e(TAG, "Cannot find matching database record for My Story.") + throw MyStoryDoesNotExistException() + } + + if (matching != null) { + Log.d(TAG, "Found a matching RecipientId for the distribution list...") + val recordForSync = SignalDatabase.recipients.getRecordForSync(matching) + if (recordForSync == null) { + Log.e(TAG, "Could not find a record for the recipient id in the recipient table") + throw IllegalStateException("Found matching recipient but couldn't generate record for sync.") + } + + if (recordForSync.recipientType.id != RecipientTable.RecipientType.DISTRIBUTION_LIST.id) { + Log.d(TAG, "Record has an incorrect group type.") + throw InvalidGroupTypeException() + } + + val record = StorageSyncModels.localToRemoteRecord(recordForSync).storyDistributionList + if (record.isPresent) { + Log.d(TAG, "Found a matching record.") + return record + } else { + Log.e(TAG, "Could not resolve the record") + throw UnexpectedEmptyOptionalException() + } + } else { + Log.d(TAG, "Could not find a matching record. Returning an empty.") + return Optional.empty() + } + } + + override fun merge(remote: SignalStoryDistributionListRecord, local: SignalStoryDistributionListRecord, keyGenerator: StorageKeyGenerator): SignalStoryDistributionListRecord { + val unknownFields = remote.serializeUnknownFields() + val identifier = remote.identifier + val name = remote.name + val recipients = remote.recipients + val deletedAtTimestamp = remote.deletedAtTimestamp + val allowsReplies = remote.allowsReplies() + val isBlockList = remote.isBlockList + + val matchesRemote = doParamsMatch( + record = remote, + unknownFields = unknownFields, + identifier = identifier, + name = name, + recipients = recipients, + deletedAtTimestamp = deletedAtTimestamp, + allowsReplies = allowsReplies, + isBlockList = isBlockList + ) + val matchesLocal = doParamsMatch( + record = local, + unknownFields = unknownFields, + identifier = identifier, + name = name, + recipients = recipients, + deletedAtTimestamp = deletedAtTimestamp, + allowsReplies = allowsReplies, + isBlockList = isBlockList + ) + + return if (matchesRemote) { + remote + } else if (matchesLocal) { + local + } else { + SignalStoryDistributionListRecord.Builder(keyGenerator.generate(), unknownFields) + .setIdentifier(identifier) + .setName(name) + .setRecipients(recipients) + .setDeletedAtTimestamp(deletedAtTimestamp) + .setAllowsReplies(allowsReplies) + .setIsBlockList(isBlockList) + .build() + } + } + + @Throws(IOException::class) + override fun insertLocal(record: SignalStoryDistributionListRecord) { + SignalDatabase.distributionLists.applyStorageSyncStoryDistributionListInsert(record) + } + + override fun updateLocal(update: StorageRecordUpdate) { + SignalDatabase.distributionLists.applyStorageSyncStoryDistributionListUpdate(update) + } + + override fun compare(o1: SignalStoryDistributionListRecord, o2: SignalStoryDistributionListRecord): Int { + return if (o1.identifier.contentEquals(o2.identifier)) { + 0 + } else { + 1 + } + } + + private fun doParamsMatch( + record: SignalStoryDistributionListRecord, + unknownFields: ByteArray?, + identifier: ByteArray?, + name: String?, + recipients: List, + deletedAtTimestamp: Long, + allowsReplies: Boolean, + isBlockList: Boolean + ): Boolean { + return unknownFields.contentEquals(record.serializeUnknownFields()) && + identifier.contentEquals(record.identifier) && + name == record.name && + recipients == record.recipients && + deletedAtTimestamp == record.deletedAtTimestamp && + allowsReplies == record.allowsReplies() && + isBlockList == record.isBlockList + } + + /** + * Thrown when the RecipientSettings object for a given distribution list is not the + * correct group type (4). + */ + private class InvalidGroupTypeException : RuntimeException() + + /** + * Thrown when the distribution list object returned from the storage sync helper is + * absent, even though a RecipientSettings was found. + */ + private class UnexpectedEmptyOptionalException : RuntimeException() + + /** + * Thrown when we try to ge the matching record for the "My Story" distribution ID but + * it isn't in the database. + */ + private class MyStoryDoesNotExistException : RuntimeException() +}