Remove cruft around SignalAccountRecord.

This commit is contained in:
Greyson Parrelli 2024-11-11 11:01:34 -05:00
parent 5e8318d63f
commit ae37c4019f
30 changed files with 536 additions and 1523 deletions

View file

@ -37,7 +37,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
val oldRecord: SignalContactRecord = StorageSyncModels.localToRemoteRecord(SignalDatabase.recipients.getRecordForSync(harness.others[0])!!).contact.get()
val newProto = oldRecord
.toProto()
.proto
.newBuilder()
.identityState(ContactRecord.IdentityState.DEFAULT)
.build()

View file

@ -17,8 +17,11 @@ import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.exists
import org.signal.core.util.forEach
import org.signal.core.util.hasUnknownFields
import org.signal.core.util.isNotEmpty
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.core.util.nullIfEmpty
import org.signal.core.util.optionalString
import org.signal.core.util.or
import org.signal.core.util.orNull
@ -1024,12 +1027,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate<SignalAccountRecord>) {
val profileName = ProfileName.fromParts(update.new.givenName.orElse(null), update.new.familyName.orElse(null))
val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orElse(null))
val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orElse(null))
val profileKey: String? = remoteKey.or(localKey).map { obj: ProfileKey -> obj.serialize() }.map { source: ByteArray? -> Base64.encodeWithPadding(source!!) }.orElse(null)
if (!remoteKey.isPresent) {
Log.w(TAG, "Got an empty profile key while applying an account record update! The parsed local key is ${if (localKey.isPresent) "present" else "not present"}. The raw local key is ${if (update.old.profileKey.isPresent) "present" else "not present"}. The resulting key is ${if (profileKey != null) "present" else "not present"}.")
val profileName = ProfileName.fromParts(update.new.proto.givenName, update.new.proto.familyName)
val localKey = update.old.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) }
val remoteKey = update.new.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) }
val profileKey: String? = (remoteKey ?: localKey)?.let { Base64.encodeWithPadding(it.serialize()) }
if (remoteKey == null) {
Log.w(TAG, "Got an empty profile key while applying an account record update! The parsed local key is ${if (localKey != null) "present" else "not present"}. The raw local key is ${if (update.old.proto.profileKey.isNotEmpty()) "present" else "not present"}. The resulting key is ${if (profileKey != null) "present" else "not present"}.")
}
val values = ContentValues().apply {
@ -1043,21 +1047,21 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
Log.w(TAG, "Avoided attempt to apply null profile key in account record update!")
}
put(USERNAME, update.new.username)
put(USERNAME, update.new.proto.username.nullIfBlank())
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.new.id.raw))
if (update.new.hasUnknownFields()) {
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(update.new.serializeUnknownFields())))
if (update.new.proto.hasUnknownFields()) {
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(update.new.serializeUnknownFields()!!))
} else {
putNull(STORAGE_SERVICE_PROTO)
}
}
if (update.new.username != null) {
if (update.new.proto.username.nullIfBlank() != null) {
writableDatabase
.update(TABLE_NAME)
.values(USERNAME to null)
.where("$USERNAME = ?", update.new.username!!)
.where("$USERNAME = ?", update.new.proto.username)
.run()
}

View file

@ -71,10 +71,11 @@ import org.thoughtcrime.securesms.util.LRUCache
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.isScheduled
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation
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.toSignalServiceAddress
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import java.io.Closeable
import java.io.IOException
import java.util.Collections
@ -1522,7 +1523,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalAccountRecord) {
writableDatabase.withinTransaction { db ->
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived, record.isNoteToSelfForcedUnread)
applyStorageSyncUpdate(recipientId, record.proto.noteToSelfArchived, record.proto.noteToSelfMarkedUnread)
db.updateAll(TABLE_NAME)
.values(PINNED to 0)
@ -1530,19 +1531,19 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
var pinnedPosition = 1
for (pinned: PinnedConversation in record.pinnedConversations) {
val pinnedRecipient: Recipient? = if (pinned.contact.isPresent) {
Recipient.externalPush(pinned.contact.get())
} else if (pinned.groupV1Id.isPresent) {
for (pinned: AccountRecord.PinnedConversation in record.proto.pinnedConversations) {
val pinnedRecipient: Recipient? = if (pinned.contact != null) {
Recipient.externalPush(pinned.contact!!.toSignalServiceAddress())
} else if (pinned.legacyGroupId != null) {
try {
Recipient.externalGroupExact(GroupId.v1(pinned.groupV1Id.get()))
Recipient.externalGroupExact(GroupId.v1(pinned.legacyGroupId!!.toByteArray()))
} catch (e: BadGroupIdException) {
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e)
null
}
} else if (pinned.groupV2MasterKey.isPresent) {
} else if (pinned.groupMasterKey != null) {
try {
Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(pinned.groupV2MasterKey.get())))
Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(pinned.groupMasterKey!!.toByteArray())))
} catch (e: InvalidInputException) {
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e)
null

View file

@ -124,9 +124,9 @@ public class StorageAccountRestoreJob extends BaseJob {
JobManager jobManager = AppDependencies.getJobManager();
if (accountRecord.getAvatarUrlPath().isPresent()) {
if (!accountRecord.getProto().avatarUrlPath.isEmpty()) {
Log.i(TAG, "Fetching avatar...");
Optional<JobTracker.JobState> state = jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN/2);
Optional<JobTracker.JobState> state = jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getProto().avatarUrlPath), LIFESPAN / 2);
if (state.isPresent()) {
Log.i(TAG, "Avatar retrieved successfully. " + state.get());

View file

@ -100,13 +100,11 @@ import java.util.stream.Collectors
* - Update the respective model (i.e. [SignalContactRecord])
* - Add getters
* - Update the builder
* - Update [SignalRecord.describeDiff].
* - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make
* sure that you're:
* - Merging the attributes, likely preferring remote
* - Adding to doParamsMatch()
* - Adding the parameter to the builder chain when creating a merged model
* - Update builder usage in StorageSyncModels
* - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make sure that you're:
* - Merging the attributes, likely preferring remote
* - Adding to doParamsMatch()
* - Adding the parameter to the builder chain when creating a merged model
* - Update builder usage in StorageSyncModels
* - Handle the new data when writing to the local storage
* (i.e. [RecipientTable.applyStorageSyncContactUpdate]).
* - Make sure that whenever you change the field in the UI, we rotate the storageId for that row

View file

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.preference.PreferenceManager
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.Curve
@ -401,10 +402,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
var username: String?
get() {
val value = getString(KEY_USERNAME, null)
return if (value.isNullOrBlank()) null else value
return value.nullIfBlank()
}
set(value) {
putString(KEY_USERNAME, value)
putString(KEY_USERNAME, value.nullIfBlank())
}
/** The local user's username link components, if set. */

View file

@ -1,14 +1,20 @@
package org.thoughtcrime.securesms.storage
import android.content.Context
import okio.ByteString
import org.signal.core.util.isNotEmpty
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfEmpty
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates
import org.thoughtcrime.securesms.storage.StorageSyncHelper.buildAccountRecord
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
import org.whispersystems.signalservice.api.util.OptionalUtil
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.storage.safeSetBackupsSubscriber
import org.whispersystems.signalservice.api.storage.safeSetPayments
import org.whispersystems.signalservice.api.storage.safeSetSubscriber
import org.whispersystems.signalservice.api.storage.toSignalAccountRecord
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool
import java.util.Optional
@ -45,184 +51,100 @@ class AccountRecordProcessor(
return false
}
override fun getMatching(record: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional<SignalAccountRecord> {
override fun getMatching(remote: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional<SignalAccountRecord> {
return Optional.of(localAccountRecord)
}
override fun merge(remote: SignalAccountRecord, local: SignalAccountRecord, keyGenerator: StorageKeyGenerator): SignalAccountRecord {
val givenName: String
val familyName: String
val mergedGivenName: String
val mergedFamilyName: String
if (remote.givenName.isPresent || remote.familyName.isPresent) {
givenName = remote.givenName.orElse("")
familyName = remote.familyName.orElse("")
if (remote.proto.givenName.isNotBlank() || remote.proto.familyName.isNotBlank()) {
mergedGivenName = remote.proto.givenName
mergedFamilyName = remote.proto.familyName
} else {
givenName = local.givenName.orElse("")
familyName = local.familyName.orElse("")
mergedGivenName = local.proto.givenName
mergedFamilyName = local.proto.familyName
}
val payments = if (remote.payments.entropy.isPresent) {
remote.payments
val payments = if (remote.proto.payments?.entropy != null) {
remote.proto.payments
} else {
local.payments
local.proto.payments
}
val subscriber = if (remote.subscriber.id.isPresent) {
remote.subscriber
val donationSubscriberId: ByteString
val donationSubscriberCurrencyCode: String
if (remote.proto.subscriberId.isNotEmpty()) {
donationSubscriberId = remote.proto.subscriberId
donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode
} else {
local.subscriber
donationSubscriberId = local.proto.subscriberId
donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode
}
val backupsSubscriber = if (remote.subscriber.id.isPresent) {
remote.subscriber
val backupsSubscriberId: ByteString
val backupsSubscriberCurrencyCode: String
if (remote.proto.backupsSubscriberId.isNotEmpty()) {
backupsSubscriberId = remote.proto.backupsSubscriberId
backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode
} else {
local.subscriber
backupsSubscriberId = local.proto.backupsSubscriberId
backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode
}
val storyViewReceiptsState = if (remote.storyViewReceiptsState == OptionalBool.UNSET) {
local.storyViewReceiptsState
val storyViewReceiptsState = if (remote.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) {
local.proto.storyViewReceiptsEnabled
} else {
remote.storyViewReceiptsState
remote.proto.storyViewReceiptsEnabled
}
val unknownFields = remote.serializeUnknownFields()
val avatarUrlPath = OptionalUtil.or(remote.avatarUrlPath, local.avatarUrlPath).orElse("")
val profileKey = OptionalUtil.or(remote.profileKey, local.profileKey).orElse(null)
val noteToSelfArchived = remote.isNoteToSelfArchived
val noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread
val readReceipts = remote.isReadReceiptsEnabled
val typingIndicators = remote.isTypingIndicatorsEnabled
val sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled
val linkPreviews = remote.isLinkPreviewsEnabled
val unlisted = remote.isPhoneNumberUnlisted
val pinnedConversations = remote.pinnedConversations
val phoneNumberSharingMode = remote.phoneNumberSharingMode
val preferContactAvatars = remote.isPreferContactAvatars
val universalExpireTimer = remote.universalExpireTimer
val primarySendsSms = if (SignalStore.account.isPrimaryDevice) local.isPrimarySendsSms else remote.isPrimarySendsSms
val e164 = if (SignalStore.account.isPrimaryDevice) local.e164 else remote.e164
val defaultReactions = if (remote.defaultReactions.size > 0) remote.defaultReactions else local.defaultReactions
val displayBadgesOnProfile = remote.isDisplayBadgesOnProfile
val subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled
val keepMutedChatsArchived = remote.isKeepMutedChatsArchived
val hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy()
val hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory()
val storiesDisabled = remote.isStoriesDisabled
val hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet() || local.hasSeenGroupStoryEducationSheet()
val hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding() || local.hasCompletedUsernameOnboarding()
val username = remote.username
val usernameLink = remote.usernameLink
val matchesRemote = doParamsMatch(
contact = remote,
unknownFields = unknownFields,
givenName = givenName,
familyName = familyName,
avatarUrlPath = avatarUrlPath,
profileKey = profileKey,
noteToSelfArchived = noteToSelfArchived,
noteToSelfForcedUnread = noteToSelfForcedUnread,
readReceipts = readReceipts,
typingIndicators = typingIndicators,
sealedSenderIndicators = sealedSenderIndicators,
linkPreviewsEnabled = linkPreviews,
phoneNumberSharingMode = phoneNumberSharingMode,
unlistedPhoneNumber = unlisted,
pinnedConversations = pinnedConversations,
preferContactAvatars = preferContactAvatars,
payments = payments,
universalExpireTimer = universalExpireTimer,
primarySendsSms = primarySendsSms,
e164 = e164,
defaultReactions = defaultReactions,
subscriber = subscriber,
displayBadgesOnProfile = displayBadgesOnProfile,
subscriptionManuallyCancelled = subscriptionManuallyCancelled,
keepMutedChatsArchived = keepMutedChatsArchived,
hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy,
hasViewedOnboardingStory = hasViewedOnboardingStory,
hasCompletedUsernameOnboarding = hasSeenUsernameOnboarding,
storiesDisabled = storiesDisabled,
storyViewReceiptsState = storyViewReceiptsState,
username = username,
usernameLink = usernameLink,
backupsSubscriber = backupsSubscriber
)
val matchesLocal = doParamsMatch(
contact = local,
unknownFields = unknownFields,
givenName = givenName,
familyName = familyName,
avatarUrlPath = avatarUrlPath,
profileKey = profileKey,
noteToSelfArchived = noteToSelfArchived,
noteToSelfForcedUnread = noteToSelfForcedUnread,
readReceipts = readReceipts,
typingIndicators = typingIndicators,
sealedSenderIndicators = sealedSenderIndicators,
linkPreviewsEnabled = linkPreviews,
phoneNumberSharingMode = phoneNumberSharingMode,
unlistedPhoneNumber = unlisted,
pinnedConversations = pinnedConversations,
preferContactAvatars = preferContactAvatars,
payments = payments,
universalExpireTimer = universalExpireTimer,
primarySendsSms = primarySendsSms,
e164 = e164,
defaultReactions = defaultReactions,
subscriber = subscriber,
displayBadgesOnProfile = displayBadgesOnProfile,
subscriptionManuallyCancelled = subscriptionManuallyCancelled,
keepMutedChatsArchived = keepMutedChatsArchived,
hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy,
hasViewedOnboardingStory = hasViewedOnboardingStory,
hasCompletedUsernameOnboarding = hasSeenUsernameOnboarding,
storiesDisabled = storiesDisabled,
storyViewReceiptsState = storyViewReceiptsState,
username = username,
usernameLink = usernameLink,
backupsSubscriber = backupsSubscriber
)
val merged = SignalAccountRecord.newBuilder(unknownFields).apply {
givenName = mergedGivenName
familyName = mergedFamilyName
avatarUrlPath = remote.proto.avatarUrlPath.nullIfEmpty() ?: local.proto.avatarUrlPath
profileKey = remote.proto.profileKey.nullIfEmpty() ?: local.proto.profileKey
noteToSelfArchived = remote.proto.noteToSelfArchived
noteToSelfMarkedUnread = remote.proto.noteToSelfMarkedUnread
readReceipts = remote.proto.readReceipts
typingIndicators = remote.proto.typingIndicators
sealedSenderIndicators = remote.proto.sealedSenderIndicators
linkPreviews = remote.proto.linkPreviews
unlistedPhoneNumber = remote.proto.unlistedPhoneNumber
pinnedConversations = remote.proto.pinnedConversations
phoneNumberSharingMode = remote.proto.phoneNumberSharingMode
preferContactAvatars = remote.proto.preferContactAvatars
universalExpireTimer = remote.proto.universalExpireTimer
primarySendsSms = false
e164 = if (SignalStore.account.isPrimaryDevice) local.proto.e164 else remote.proto.e164
preferredReactionEmoji = remote.proto.preferredReactionEmoji.takeIf { it.isNotEmpty() } ?: local.proto.preferredReactionEmoji
displayBadgesOnProfile = remote.proto.displayBadgesOnProfile
subscriptionManuallyCancelled = remote.proto.subscriptionManuallyCancelled
keepMutedChatsArchived = remote.proto.keepMutedChatsArchived
hasSetMyStoriesPrivacy = remote.proto.hasSetMyStoriesPrivacy
hasViewedOnboardingStory = remote.proto.hasViewedOnboardingStory || local.proto.hasViewedOnboardingStory
storiesDisabled = remote.proto.storiesDisabled
storyViewReceiptsEnabled = storyViewReceiptsState
hasSeenGroupStoryEducationSheet = remote.proto.hasSeenGroupStoryEducationSheet || local.proto.hasSeenGroupStoryEducationSheet
hasCompletedUsernameOnboarding = remote.proto.hasCompletedUsernameOnboarding || local.proto.hasCompletedUsernameOnboarding
username = remote.proto.username
usernameLink = remote.proto.usernameLink
if (matchesRemote) {
return remote
} else if (matchesLocal) {
return local
safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray())
safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode)
safeSetBackupsSubscriber(backupsSubscriberId, backupsSubscriberCurrencyCode)
}.toSignalAccountRecord(StorageId.forAccount(keyGenerator.generate()))
return if (doParamsMatch(remote, merged)) {
remote
} else if (doParamsMatch(local, merged)) {
local
} else {
val builder = SignalAccountRecord.Builder(keyGenerator.generate(), unknownFields)
.setGivenName(givenName)
.setFamilyName(familyName)
.setAvatarUrlPath(avatarUrlPath)
.setProfileKey(profileKey)
.setNoteToSelfArchived(noteToSelfArchived)
.setNoteToSelfForcedUnread(noteToSelfForcedUnread)
.setReadReceiptsEnabled(readReceipts)
.setTypingIndicatorsEnabled(typingIndicators)
.setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
.setLinkPreviewsEnabled(linkPreviews)
.setUnlistedPhoneNumber(unlisted)
.setPhoneNumberSharingMode(phoneNumberSharingMode)
.setUnlistedPhoneNumber(unlisted)
.setPinnedConversations(pinnedConversations)
.setPreferContactAvatars(preferContactAvatars)
.setPayments(payments.isEnabled, payments.entropy.orElse(null))
.setUniversalExpireTimer(universalExpireTimer)
.setPrimarySendsSms(primarySendsSms)
.setDefaultReactions(defaultReactions)
.setSubscriber(subscriber)
.setStoryViewReceiptsState(storyViewReceiptsState)
.setDisplayBadgesOnProfile(displayBadgesOnProfile)
.setSubscriptionManuallyCancelled(subscriptionManuallyCancelled)
.setKeepMutedChatsArchived(keepMutedChatsArchived)
.setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy)
.setHasViewedOnboardingStory(hasViewedOnboardingStory)
.setStoriesDisabled(storiesDisabled)
.setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation)
.setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding)
.setUsername(username)
.setUsernameLink(usernameLink)
.setBackupsSubscriber(backupsSubscriber)
return builder.build()
merged
}
}
@ -238,72 +160,7 @@ class AccountRecordProcessor(
return 0
}
private fun doParamsMatch(
contact: SignalAccountRecord,
unknownFields: ByteArray?,
givenName: String,
familyName: String,
avatarUrlPath: String,
profileKey: ByteArray?,
noteToSelfArchived: Boolean,
noteToSelfForcedUnread: Boolean,
readReceipts: Boolean,
typingIndicators: Boolean,
sealedSenderIndicators: Boolean,
linkPreviewsEnabled: Boolean,
phoneNumberSharingMode: AccountRecord.PhoneNumberSharingMode,
unlistedPhoneNumber: Boolean,
pinnedConversations: List<SignalAccountRecord.PinnedConversation>,
preferContactAvatars: Boolean,
payments: SignalAccountRecord.Payments,
universalExpireTimer: Int,
primarySendsSms: Boolean,
e164: String,
defaultReactions: List<String>,
subscriber: SignalAccountRecord.Subscriber,
displayBadgesOnProfile: Boolean,
subscriptionManuallyCancelled: Boolean,
keepMutedChatsArchived: Boolean,
hasSetMyStoriesPrivacy: Boolean,
hasViewedOnboardingStory: Boolean,
hasCompletedUsernameOnboarding: Boolean,
storiesDisabled: Boolean,
storyViewReceiptsState: OptionalBool,
username: String?,
usernameLink: AccountRecord.UsernameLink?,
backupsSubscriber: SignalAccountRecord.Subscriber
): Boolean {
return contact.serializeUnknownFields().contentEquals(unknownFields) &&
contact.givenName.orElse("") == givenName &&
contact.familyName.orElse("") == familyName &&
contact.avatarUrlPath.orElse("") == avatarUrlPath &&
contact.payments == payments &&
contact.e164 == e164 &&
contact.defaultReactions == defaultReactions &&
contact.profileKey.orElse(null).contentEquals(profileKey) &&
contact.isNoteToSelfArchived == noteToSelfArchived &&
contact.isNoteToSelfForcedUnread == noteToSelfForcedUnread &&
contact.isReadReceiptsEnabled == readReceipts &&
contact.isTypingIndicatorsEnabled == typingIndicators &&
contact.isSealedSenderIndicatorsEnabled == sealedSenderIndicators &&
contact.isLinkPreviewsEnabled == linkPreviewsEnabled &&
contact.phoneNumberSharingMode == phoneNumberSharingMode &&
contact.isPhoneNumberUnlisted == unlistedPhoneNumber &&
contact.isPreferContactAvatars == preferContactAvatars &&
contact.universalExpireTimer == universalExpireTimer &&
contact.isPrimarySendsSms == primarySendsSms &&
contact.pinnedConversations == pinnedConversations &&
contact.subscriber == subscriber &&
contact.isDisplayBadgesOnProfile == displayBadgesOnProfile &&
contact.isSubscriptionManuallyCancelled == subscriptionManuallyCancelled &&
contact.isKeepMutedChatsArchived == keepMutedChatsArchived &&
contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy &&
contact.hasViewedOnboardingStory() == hasViewedOnboardingStory &&
contact.hasCompletedUsernameOnboarding() == hasCompletedUsernameOnboarding &&
contact.isStoriesDisabled == storiesDisabled &&
contact.storyViewReceiptsState == storyViewReceiptsState &&
contact.username == username &&
contact.usernameLink == usernameLink &&
contact.backupsSubscriber == backupsSubscriber
private fun doParamsMatch(base: SignalAccountRecord, test: SignalAccountRecord): Boolean {
return base.serializeUnknownFields().contentEquals(test.serializeUnknownFields()) && base.proto == test.proto
}
}

View file

@ -15,7 +15,7 @@ import java.util.TreeSet
* our local store). We use it for a [TreeSet], so mainly it's just important that the '0'
* case is correct. Other cases are whatever, just make it something stable.
*/
abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordProcessor<E>, Comparator<E> {
abstract class DefaultStorageRecordProcessor<E : SignalRecord<*>> : StorageRecordProcessor<E>, Comparator<E> {
companion object {
private val TAG = Log.tag(DefaultStorageRecordProcessor::class.java)
}
@ -37,16 +37,15 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordPr
@Throws(IOException::class)
override fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator) {
val matchedRecords: MutableSet<E> = TreeSet(this)
var i = 0
for (remote in remoteRecords) {
for ((i, remote) in remoteRecords.withIndex()) {
if (isInvalid(remote)) {
warn(i, remote, "Found invalid key! Ignoring it.")
} else {
val local = getMatching(remote, keyGenerator)
if (local.isPresent) {
val merged = merge(remote, local.get(), keyGenerator)
val merged: E = merge(remote, local.get(), keyGenerator)
if (matchedRecords.contains(local.get())) {
warn(i, remote, "Multiple remote records map to the same local record! Ignoring this one.")
@ -54,7 +53,7 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordPr
matchedRecords.add(local.get())
if (merged != remote) {
info(i, remote, "[Remote Update] " + StorageRecordUpdate(remote, merged).toString())
info(i, remote, "[Remote Update] " + remote.describeDiff(merged))
}
if (merged != local.get()) {
@ -68,8 +67,6 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordPr
insertLocal(remote)
}
}
i++
}
}

View file

@ -7,7 +7,7 @@ import java.io.IOException
* Handles processing a remote record, which involves applying any local changes that need to be
* made based on the remote records.
*/
interface StorageRecordProcessor<E : SignalRecord?> {
interface StorageRecordProcessor<E : SignalRecord<*>> {
@Throws(IOException::class)
fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator)
}

View file

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import org.whispersystems.signalservice.api.storage.SignalRecord;
import java.util.Objects;
/**
* Represents a pair of records: one old, and one new. The new record should replace the old.
*/
public class StorageRecordUpdate<E extends SignalRecord> {
private final E oldRecord;
private final E newRecord;
public StorageRecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) {
this.oldRecord = oldRecord;
this.newRecord = newRecord;
}
public @NonNull E getOld() {
return oldRecord;
}
public @NonNull E getNew() {
return newRecord;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StorageRecordUpdate that = (StorageRecordUpdate) o;
return oldRecord.equals(that.oldRecord) &&
newRecord.equals(that.newRecord);
}
@Override
public int hashCode() {
return Objects.hash(oldRecord, newRecord);
}
@Override
public @NonNull String toString() {
return newRecord.describeDiff(oldRecord);
}
}

View file

@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.storage
import org.whispersystems.signalservice.api.storage.SignalRecord
/**
* Represents a pair of records: one old, and one new. The new record should replace the old.
*/
class StorageRecordUpdate<E : SignalRecord<*>>(val old: E, val new: E) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StorageRecordUpdate<*>
if (old != other.old) return false
if (new != other.new) return false
return true
}
override fun hashCode(): Int {
var result = old.hashCode()
result = 31 * result + new.hashCode()
return result
}
}

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.storage
import android.content.Context
import androidx.annotation.VisibleForTesting
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64.encodeWithPadding
import org.signal.core.util.logging.Log
@ -28,6 +29,10 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.SignalStorageManifest
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.storage.safeSetBackupsSubscriber
import org.whispersystems.signalservice.api.storage.safeSetPayments
import org.whispersystems.signalservice.api.storage.safeSetSubscriber
import org.whispersystems.signalservice.api.storage.toSignalAccountRecord
import org.whispersystems.signalservice.api.util.OptionalUtil.byteArrayEquals
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
@ -130,52 +135,54 @@ object StorageSyncHelper {
val storageId = selfRecord?.storageId ?: self.storageId
val account = SignalAccountRecord.Builder(storageId, selfRecord?.syncExtras?.storageProto)
.setProfileKey(self.profileKey)
.setGivenName(self.profileName.givenName)
.setFamilyName(self.profileName.familyName)
.setAvatarUrlPath(self.profileAvatar)
.setNoteToSelfArchived(selfRecord != null && selfRecord.syncExtras.isArchived)
.setNoteToSelfForcedUnread(selfRecord != null && selfRecord.syncExtras.isForcedUnread)
.setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context))
.setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context))
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context))
.setLinkPreviewsEnabled(SignalStore.settings.isLinkPreviewsEnabled)
.setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE)
.setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode))
.setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned))
.setPreferContactAvatars(SignalStore.settings.isPreferSystemContactPhotos)
.setPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null))
.setPrimarySendsSms(false)
.setUniversalExpireTimer(SignalStore.settings.universalExpireTimer)
.setDefaultReactions(SignalStore.emoji.reactions)
.setSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)))
.setBackupsSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)))
.setDisplayBadgesOnProfile(SignalStore.inAppPayments.getDisplayBadgesOnProfile())
.setSubscriptionManuallyCancelled(isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION))
.setKeepMutedChatsArchived(SignalStore.settings.shouldKeepMutedChatsArchived())
.setHasSetMyStoriesPrivacy(SignalStore.story.userHasBeenNotifiedAboutStories)
.setHasViewedOnboardingStory(SignalStore.story.userHasViewedOnboardingStory)
.setStoriesDisabled(SignalStore.story.isFeatureDisabled)
.setStoryViewReceiptsState(storyViewReceiptsState)
.setHasSeenGroupStoryEducationSheet(SignalStore.story.userHasSeenGroupStoryEducationSheet)
.setUsername(SignalStore.account.username)
.setHasCompletedUsernameOnboarding(SignalStore.uiHints.hasCompletedUsernameOnboarding())
val accountRecord = SignalAccountRecord.newBuilder(selfRecord?.syncExtras?.storageProto).apply {
profileKey = self.profileKey?.toByteString() ?: ByteString.EMPTY
givenName = self.profileName.givenName
familyName = self.profileName.familyName
avatarUrlPath = self.profileAvatar ?: ""
noteToSelfArchived = selfRecord != null && selfRecord.syncExtras.isArchived
noteToSelfMarkedUnread = selfRecord != null && selfRecord.syncExtras.isForcedUnread
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context)
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context)
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)
linkPreviews = SignalStore.settings.isLinkPreviewsEnabled
unlistedPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
phoneNumberSharingMode = StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode)
pinnedConversations = StorageSyncModels.localToRemotePinnedConversations(pinned)
preferContactAvatars = SignalStore.settings.isPreferSystemContactPhotos
primarySendsSms = false
universalExpireTimer = SignalStore.settings.universalExpireTimer
preferredReactionEmoji = SignalStore.emoji.reactions
displayBadgesOnProfile = SignalStore.inAppPayments.getDisplayBadgesOnProfile()
subscriptionManuallyCancelled = isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)
keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived()
hasSetMyStoriesPrivacy = SignalStore.story.userHasBeenNotifiedAboutStories
hasViewedOnboardingStory = SignalStore.story.userHasViewedOnboardingStory
storiesDisabled = SignalStore.story.isFeatureDisabled
storyViewReceiptsEnabled = storyViewReceiptsState
hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet
hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding()
username = SignalStore.account.username ?: ""
usernameLink = SignalStore.account.usernameLink?.let { linkComponents ->
AccountRecord.UsernameLink(
entropy = linkComponents.entropy.toByteString(),
serverId = linkComponents.serverId.toByteArray().toByteString(),
color = StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)
)
}
val linkComponents = SignalStore.account.usernameLink
if (linkComponents != null) {
account.setUsernameLink(
AccountRecord.UsernameLink.Builder()
.entropy(linkComponents.entropy.toByteString())
.serverId(linkComponents.serverId.toByteArray().toByteString())
.color(StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme))
.build()
)
} else {
account.setUsernameLink(null)
getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let {
safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
}
getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)?.let {
safeSetBackupsSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
}
safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null))
}
return SignalStorageRecord.forAccount(account.build())
return SignalStorageRecord.forAccount(accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)))
}
@JvmStatic
@ -188,62 +195,56 @@ object StorageSyncHelper {
fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, update: StorageRecordUpdate<SignalAccountRecord>, fetchProfile: Boolean) {
SignalDatabase.recipients.applyStorageSyncAccountUpdate(update)
TextSecurePreferences.setReadReceiptsEnabled(context, update.new.isReadReceiptsEnabled)
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.isTypingIndicatorsEnabled)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.isSealedSenderIndicatorsEnabled)
SignalStore.settings.isLinkPreviewsEnabled = update.new.isLinkPreviewsEnabled
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.isPhoneNumberUnlisted) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.phoneNumberSharingMode)
SignalStore.settings.isPreferSystemContactPhotos = update.new.isPreferContactAvatars
SignalStore.payments.setEnabledAndEntropy(update.new.payments.isEnabled, Entropy.fromBytes(update.new.payments.entropy.orElse(null)))
SignalStore.settings.universalExpireTimer = update.new.universalExpireTimer
SignalStore.emoji.reactions = update.new.defaultReactions
SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.isDisplayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(update.new.isKeepMutedChatsArchived)
SignalStore.story.userHasBeenNotifiedAboutStories = update.new.hasSetMyStoriesPrivacy()
SignalStore.story.userHasViewedOnboardingStory = update.new.hasViewedOnboardingStory()
SignalStore.story.isFeatureDisabled = update.new.isStoriesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.hasSeenGroupStoryEducationSheet()
SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.hasCompletedUsernameOnboarding())
TextSecurePreferences.setReadReceiptsEnabled(context, update.new.proto.readReceipts)
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.proto.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.proto.sealedSenderIndicators)
SignalStore.settings.isLinkPreviewsEnabled = update.new.proto.linkPreviews
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.proto.unlistedPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.proto.phoneNumberSharingMode)
SignalStore.settings.isPreferSystemContactPhotos = update.new.proto.preferContactAvatars
SignalStore.payments.setEnabledAndEntropy(update.new.proto.payments?.enabled == true, Entropy.fromBytes(update.new.proto.payments?.entropy?.toByteArray()))
SignalStore.settings.universalExpireTimer = update.new.proto.universalExpireTimer
SignalStore.emoji.reactions = update.new.proto.preferredReactionEmoji
SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.proto.displayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(update.new.proto.keepMutedChatsArchived)
SignalStore.story.userHasBeenNotifiedAboutStories = update.new.proto.hasSetMyStoriesPrivacy
SignalStore.story.userHasViewedOnboardingStory = update.new.proto.hasViewedOnboardingStory
SignalStore.story.isFeatureDisabled = update.new.proto.storiesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.proto.hasSeenGroupStoryEducationSheet
SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.proto.hasCompletedUsernameOnboarding)
if (update.new.storyViewReceiptsState == OptionalBool.UNSET) {
SignalStore.story.viewedReceiptsEnabled = update.new.isReadReceiptsEnabled
if (update.new.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) {
SignalStore.story.viewedReceiptsEnabled = update.new.proto.readReceipts
} else {
SignalStore.story.viewedReceiptsEnabled = update.new.storyViewReceiptsState == OptionalBool.ENABLED
SignalStore.story.viewedReceiptsEnabled = update.new.proto.storyViewReceiptsEnabled == OptionalBool.ENABLED
}
if (update.new.storyViewReceiptsState == OptionalBool.UNSET) {
SignalStore.story.viewedReceiptsEnabled = update.new.isReadReceiptsEnabled
} else {
SignalStore.story.viewedReceiptsEnabled = update.new.storyViewReceiptsState == OptionalBool.ENABLED
}
val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.subscriber, InAppPaymentSubscriberRecord.Type.DONATION)
val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.proto.subscriberId, update.new.proto.subscriberCurrencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
if (remoteSubscriber != null) {
setSubscriber(remoteSubscriber)
}
if (update.new.isSubscriptionManuallyCancelled && !update.old.isSubscriptionManuallyCancelled) {
if (update.new.proto.subscriptionManuallyCancelled && !update.old.proto.subscriptionManuallyCancelled) {
SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
}
if (fetchProfile && update.new.avatarUrlPath.isPresent) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.avatarUrlPath.get()))
if (fetchProfile && update.new.proto.avatarUrlPath.isNotBlank()) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.proto.avatarUrlPath))
}
if (update.new.username != update.old.username) {
SignalStore.account.username = update.new.username
if (update.new.proto.username != update.old.proto.username) {
SignalStore.account.username = update.new.proto.username
SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account.usernameSyncErrorCount = 0
}
if (update.new.usernameLink != null) {
if (update.new.proto.usernameLink != null) {
SignalStore.account.usernameLink = UsernameLinkComponents(
update.new.usernameLink!!.entropy.toByteArray(),
UuidUtil.parseOrThrow(update.new.usernameLink!!.serverId.toByteArray())
update.new.proto.usernameLink!!.entropy.toByteArray(),
UuidUtil.parseOrThrow(update.new.proto.usernameLink!!.serverId.toByteArray())
)
SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.usernameLink!!.color)
SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.proto.usernameLink!!.color)
}
}

View file

@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.storage
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.isNotEmpty
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState
@ -16,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
@ -82,18 +84,33 @@ object StorageSyncModels {
}
@JvmStatic
fun localToRemotePinnedConversations(records: List<RecipientRecord>): List<SignalAccountRecord.PinnedConversation> {
fun localToRemotePinnedConversations(records: List<RecipientRecord>): List<AccountRecord.PinnedConversation> {
return records
.filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED }
.map { localToRemotePinnedConversation(it) }
}
@JvmStatic
private fun localToRemotePinnedConversation(settings: RecipientRecord): SignalAccountRecord.PinnedConversation {
private fun localToRemotePinnedConversation(settings: RecipientRecord): AccountRecord.PinnedConversation {
return when (settings.recipientType) {
RecipientType.INDIVIDUAL -> SignalAccountRecord.PinnedConversation.forContact(SignalServiceAddress(settings.serviceId, settings.e164))
RecipientType.GV1 -> SignalAccountRecord.PinnedConversation.forGroupV1(settings.groupId!!.requireV1().decodedId)
RecipientType.GV2 -> SignalAccountRecord.PinnedConversation.forGroupV2(settings.syncExtras.groupMasterKey!!.serialize())
RecipientType.INDIVIDUAL -> {
AccountRecord.PinnedConversation(
contact = AccountRecord.PinnedConversation.Contact(
serviceId = settings.serviceId?.toString() ?: "",
e164 = settings.e164 ?: ""
)
)
}
RecipientType.GV1 -> {
AccountRecord.PinnedConversation(
legacyGroupId = settings.groupId!!.requireV1().decodedId.toByteString()
)
}
RecipientType.GV2 -> {
AccountRecord.PinnedConversation(
groupMasterKey = settings.syncExtras.groupMasterKey!!.serialize().toByteString()
)
}
else -> throw AssertionError("Unexpected group type!")
}
}
@ -271,33 +288,23 @@ object StorageSyncModels {
}
}
/**
* TODO - need to store the subscriber type
*/
fun localToRemoteSubscriber(subscriber: InAppPaymentSubscriberRecord?): SignalAccountRecord.Subscriber {
return if (subscriber == null) {
SignalAccountRecord.Subscriber(null, null)
} else {
SignalAccountRecord.Subscriber(subscriber.currency.currencyCode, subscriber.subscriberId.bytes)
}
}
fun remoteToLocalSubscriber(
subscriber: SignalAccountRecord.Subscriber,
subscriberId: ByteString,
subscriberCurrencyCode: String,
type: InAppPaymentSubscriberRecord.Type
): InAppPaymentSubscriberRecord? {
if (subscriber.id.isPresent) {
val subscriberId = SubscriberId.fromBytes(subscriber.id.get())
if (subscriberId.isNotEmpty()) {
val subscriberId = SubscriberId.fromBytes(subscriberId.toByteArray())
val localSubscriberRecord = inAppPaymentSubscribers.getBySubscriberId(subscriberId)
val requiresCancel = localSubscriberRecord != null && localSubscriberRecord.requiresCancel
val paymentMethodType = localSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
val currency: Currency
if (subscriber.currencyCode.isEmpty) {
if (subscriberCurrencyCode.isBlank()) {
return null
} else {
try {
currency = Currency.getInstance(subscriber.currencyCode.get())
currency = Currency.getInstance(subscriberCurrencyCode)
} catch (e: IllegalArgumentException) {
return null
}

View file

@ -177,7 +177,7 @@ public final class StorageSyncValidations {
}
}
if (insert.getAccount().isPresent() && !insert.getAccount().get().getProfileKey().isPresent()) {
if (insert.getAccount().isPresent() && insert.getAccount().get().getProto().profileKey.size() == 0) {
Log.w(TAG, "Uploading a null profile key in our AccountRecord!");
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.storage
import junit.framework.TestCase.assertEquals
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.junit.Test
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
class StorageRecordTest {
@Test
fun `describeDiff - general test`() {
val a = SignalAccountRecord(
StorageId.forAccount(Util.getSecretBytes(16)),
AccountRecord(
profileKey = ByteString.EMPTY,
givenName = "First",
familyName = "Last"
)
)
val b = SignalAccountRecord(
StorageId.forAccount(Util.getSecretBytes(16)),
AccountRecord(
profileKey = Util.getSecretBytes(16).toByteString(),
givenName = "First",
familyName = "LastB"
)
)
assertEquals("Some fields differ: familyName, id, profileKey", a.describeDiff(b))
}
@Test
fun `describeDiff - different class`() {
val a = SignalAccountRecord(
StorageId.forAccount(Util.getSecretBytes(16)),
AccountRecord()
)
val b = SignalContactRecord(
StorageId.forAccount(Util.getSecretBytes(16)),
ContactRecord()
)
assertEquals("Classes are different!", a.describeDiff(b))
}
}

View file

@ -180,7 +180,7 @@ public final class StorageSyncHelperTest {
.setProfileGivenName(profileName);
}
private static <E extends SignalRecord> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
private static <E extends SignalRecord<?>> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageRecordUpdate<>(oldRecord, newRecord);
}

View file

@ -32,6 +32,14 @@ fun ByteString?.isNullOrEmpty(): Boolean {
return this == null || this.size == 0
}
fun ByteString.nullIfEmpty(): ByteString? {
return if (this.isEmpty()) {
null
} else {
this
}
}
/**
* Performs the common pattern of attempting to decode a serialized proto and returning null if it fails to decode.
*/

View file

@ -33,6 +33,7 @@ java {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
freeCompilerArgs = listOf("-Xjvm-default=all")
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.storage
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.isNotEmpty
import org.whispersystems.signalservice.api.payments.PaymentsConstants
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import org.whispersystems.signalservice.internal.storage.protos.Payments
fun AccountRecord.Builder.safeSetPayments(enabled: Boolean, entropy: ByteArray?): AccountRecord.Builder {
val paymentsBuilder = Payments.Builder()
val entropyPresent = entropy != null && entropy.size == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH
paymentsBuilder.enabled(enabled && entropyPresent)
if (entropyPresent) {
paymentsBuilder.entropy(entropy!!.toByteString())
}
this.payments = paymentsBuilder.build()
return this
}
fun AccountRecord.Builder.safeSetSubscriber(subscriberId: ByteString, subscriberCurrencyCode: String): AccountRecord.Builder {
if (subscriberId.isNotEmpty() && subscriberId.size == 32 && subscriberCurrencyCode.isNotBlank()) {
this.subscriberId = subscriberId
this.subscriberCurrencyCode = subscriberCurrencyCode
} else {
this.subscriberId = defaultAccountRecord.subscriberId
this.subscriberCurrencyCode = defaultAccountRecord.subscriberCurrencyCode
}
return this
}
fun AccountRecord.Builder.safeSetBackupsSubscriber(subscriberId: ByteString, subscriberCurrencyCode: String): AccountRecord.Builder {
if (subscriberId.isNotEmpty() && subscriberId.size == 32 && subscriberCurrencyCode.isNotBlank()) {
this.backupsSubscriberId = subscriberId
this.backupsSubscriberCurrencyCode = subscriberCurrencyCode
} else {
this.backupsSubscriberId = defaultAccountRecord.backupsSubscriberId
this.backupsSubscriberCurrencyCode = defaultAccountRecord.backupsSubscriberCurrencyCode
}
return this
}
fun AccountRecord.Builder.toSignalAccountRecord(storageId: StorageId): SignalAccountRecord {
return SignalAccountRecord(storageId, this.build())
}
fun AccountRecord.PinnedConversation.Contact.toSignalServiceAddress(): SignalServiceAddress {
val serviceId = ServiceId.parseOrNull(this.serviceId)
return SignalServiceAddress(serviceId, this.e164)
}

View file

@ -1,769 +0,0 @@
package org.whispersystems.signalservice.api.storage;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.payments.PaymentsConstants;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import okio.ByteString;
public final class SignalAccountRecord implements SignalRecord {
private static final String TAG = SignalAccountRecord.class.getSimpleName();
private final StorageId id;
private final AccountRecord proto;
private final boolean hasUnknownFields;
private final Optional<String> givenName;
private final Optional<String> familyName;
private final Optional<String> avatarUrlPath;
private final Optional<byte[]> profileKey;
private final List<PinnedConversation> pinnedConversations;
private final Payments payments;
private final List<String> defaultReactions;
private final Subscriber subscriber;
private final Subscriber backupsSubscriber;
public SignalAccountRecord(StorageId id, AccountRecord proto) {
this.id = id;
this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.givenName = OptionalUtil.absentIfEmpty(proto.givenName);
this.familyName = OptionalUtil.absentIfEmpty(proto.familyName);
this.profileKey = OptionalUtil.absentIfEmpty(proto.profileKey);
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.avatarUrlPath);
this.pinnedConversations = new ArrayList<>(proto.pinnedConversations.size());
this.defaultReactions = new ArrayList<>(proto.preferredReactionEmoji);
this.subscriber = new Subscriber(proto.subscriberCurrencyCode, proto.subscriberId.toByteArray());
this.backupsSubscriber = new Subscriber(proto.backupsSubscriberCurrencyCode, proto.backupsSubscriberId.toByteArray());
if (proto.payments != null) {
this.payments = new Payments(proto.payments.enabled, OptionalUtil.absentIfEmpty(proto.payments.entropy));
} else {
this.payments = new Payments(false, Optional.empty());
}
for (AccountRecord.PinnedConversation conversation : proto.pinnedConversations) {
pinnedConversations.add(PinnedConversation.fromRemote(conversation));
}
}
@Override
public StorageId getId() {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forAccount(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalAccountRecord) {
SignalAccountRecord that = (SignalAccountRecord) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
diff.add("ID");
}
if (!Objects.equals(this.givenName, that.givenName)) {
diff.add("GivenName");
}
if (!Objects.equals(this.familyName, that.familyName)) {
diff.add("FamilyName");
}
if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) {
diff.add("ProfileKey");
}
if (!Objects.equals(this.avatarUrlPath, that.avatarUrlPath)) {
diff.add("AvatarUrlPath");
}
if (!Objects.equals(this.isNoteToSelfArchived(), that.isNoteToSelfArchived())) {
diff.add("NoteToSelfArchived");
}
if (!Objects.equals(this.isNoteToSelfForcedUnread(), that.isNoteToSelfForcedUnread())) {
diff.add("NoteToSelfForcedUnread");
}
if (!Objects.equals(this.isReadReceiptsEnabled(), that.isReadReceiptsEnabled())) {
diff.add("ReadReceipts");
}
if (!Objects.equals(this.isTypingIndicatorsEnabled(), that.isTypingIndicatorsEnabled())) {
diff.add("TypingIndicators");
}
if (!Objects.equals(this.isSealedSenderIndicatorsEnabled(), that.isSealedSenderIndicatorsEnabled())) {
diff.add("SealedSenderIndicators");
}
if (!Objects.equals(this.isLinkPreviewsEnabled(), that.isLinkPreviewsEnabled())) {
diff.add("LinkPreviews");
}
if (!Objects.equals(this.getPhoneNumberSharingMode(), that.getPhoneNumberSharingMode())) {
diff.add("PhoneNumberSharingMode");
}
if (!Objects.equals(this.isPhoneNumberUnlisted(), that.isPhoneNumberUnlisted())) {
diff.add("PhoneNumberUnlisted");
}
if (!Objects.equals(this.pinnedConversations, that.pinnedConversations)) {
diff.add("PinnedConversations");
}
if (!Objects.equals(this.isPreferContactAvatars(), that.isPreferContactAvatars())) {
diff.add("PreferContactAvatars");
}
if (!Objects.equals(this.payments, that.payments)) {
diff.add("Payments");
}
if (this.getUniversalExpireTimer() != that.getUniversalExpireTimer()) {
diff.add("UniversalExpireTimer");
}
if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) {
diff.add("PrimarySendsSms");
}
if (!Objects.equals(this.getE164(), that.getE164())) {
diff.add("E164");
}
if (!Objects.equals(this.getDefaultReactions(), that.getDefaultReactions())) {
diff.add("DefaultReactions");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
if (!Objects.equals(this.getSubscriber(), that.getSubscriber())) {
diff.add("Subscriber");
}
if (!Objects.equals(this.isDisplayBadgesOnProfile(), that.isDisplayBadgesOnProfile())) {
diff.add("DisplayBadgesOnProfile");
}
if (!Objects.equals(this.isSubscriptionManuallyCancelled(), that.isSubscriptionManuallyCancelled())) {
diff.add("SubscriptionManuallyCancelled");
}
if (isKeepMutedChatsArchived() != that.isKeepMutedChatsArchived()) {
diff.add("KeepMutedChatsArchived");
}
if (hasSetMyStoriesPrivacy() != that.hasSetMyStoriesPrivacy()) {
diff.add("HasSetMyStoryPrivacy");
}
if (hasViewedOnboardingStory() != that.hasViewedOnboardingStory()) {
diff.add("HasViewedOnboardingStory");
}
if (isStoriesDisabled() != that.isStoriesDisabled()) {
diff.add("StoriesDisabled");
}
if (getStoryViewReceiptsState() != that.getStoryViewReceiptsState()) {
diff.add("StoryViewedReceipts");
}
if (hasSeenGroupStoryEducationSheet() != that.hasSeenGroupStoryEducationSheet()) {
diff.add("HasSeenGroupStoryEducationSheet");
}
if (!Objects.equals(getUsername(), that.getUsername())) {
diff.add("Username");
}
if (hasCompletedUsernameOnboarding() != that.hasCompletedUsernameOnboarding()) {
diff.add("HasCompletedUsernameOnboarding");
}
if (!Objects.equals(this.getBackupsSubscriber(), that.getBackupsSubscriber())) {
diff.add("BackupsSubscriber");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.encode() : null;
}
public Optional<String> getGivenName() {
return givenName;
}
public Optional<String> getFamilyName() {
return familyName;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<String> getAvatarUrlPath() {
return avatarUrlPath;
}
public boolean isNoteToSelfArchived() {
return proto.noteToSelfArchived;
}
public boolean isNoteToSelfForcedUnread() {
return proto.noteToSelfMarkedUnread;
}
public boolean isReadReceiptsEnabled() {
return proto.readReceipts;
}
public boolean isTypingIndicatorsEnabled() {
return proto.typingIndicators;
}
public boolean isSealedSenderIndicatorsEnabled() {
return proto.sealedSenderIndicators;
}
public boolean isLinkPreviewsEnabled() {
return proto.linkPreviews;
}
public AccountRecord.PhoneNumberSharingMode getPhoneNumberSharingMode() {
return proto.phoneNumberSharingMode;
}
public boolean isPhoneNumberUnlisted() {
return proto.unlistedPhoneNumber;
}
public List<PinnedConversation> getPinnedConversations() {
return pinnedConversations;
}
public boolean isPreferContactAvatars() {
return proto.preferContactAvatars;
}
public Payments getPayments() {
return payments;
}
public int getUniversalExpireTimer() {
return proto.universalExpireTimer;
}
public boolean isPrimarySendsSms() {
return proto.primarySendsSms;
}
public String getE164() {
return proto.e164;
}
public List<String> getDefaultReactions() {
return defaultReactions;
}
public Subscriber getSubscriber() {
return subscriber;
}
public Subscriber getBackupsSubscriber() {
return backupsSubscriber;
}
public boolean isDisplayBadgesOnProfile() {
return proto.displayBadgesOnProfile;
}
public boolean isSubscriptionManuallyCancelled() {
return proto.subscriptionManuallyCancelled;
}
public boolean isKeepMutedChatsArchived() {
return proto.keepMutedChatsArchived;
}
public boolean hasSetMyStoriesPrivacy() {
return proto.hasSetMyStoriesPrivacy;
}
public boolean hasViewedOnboardingStory() {
return proto.hasViewedOnboardingStory;
}
public boolean isStoriesDisabled() {
return proto.storiesDisabled;
}
public OptionalBool getStoryViewReceiptsState() {
return proto.storyViewReceiptsEnabled;
}
public boolean hasSeenGroupStoryEducationSheet() {
return proto.hasSeenGroupStoryEducationSheet;
}
public boolean hasCompletedUsernameOnboarding() {
return proto.hasCompletedUsernameOnboarding;
}
public @Nullable String getUsername() {
return proto.username;
}
public @Nullable AccountRecord.UsernameLink getUsernameLink() {
return proto.usernameLink;
}
public AccountRecord toProto() {
return proto;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalAccountRecord that = (SignalAccountRecord) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static class PinnedConversation {
private final Optional<SignalServiceAddress> contact;
private final Optional<byte[]> groupV1Id;
private final Optional<byte[]> groupV2MasterKey;
private PinnedConversation(Optional<SignalServiceAddress> contact, Optional<byte[]> groupV1Id, Optional<byte[]> groupV2MasterKey) {
this.contact = contact;
this.groupV1Id = groupV1Id;
this.groupV2MasterKey = groupV2MasterKey;
}
public static PinnedConversation forContact(SignalServiceAddress address) {
return new PinnedConversation(Optional.of(address), Optional.empty(), Optional.empty());
}
public static PinnedConversation forGroupV1(byte[] groupId) {
return new PinnedConversation(Optional.empty(), Optional.of(groupId), Optional.empty());
}
public static PinnedConversation forGroupV2(byte[] masterKey) {
return new PinnedConversation(Optional.empty(), Optional.empty(), Optional.of(masterKey));
}
private static PinnedConversation forEmpty() {
return new PinnedConversation(Optional.empty(), Optional.empty(), Optional.empty());
}
static PinnedConversation fromRemote(AccountRecord.PinnedConversation remote) {
if (remote.contact != null) {
ServiceId serviceId = ServiceId.parseOrNull(remote.contact.serviceId);
if (serviceId != null) {
return forContact(new SignalServiceAddress(serviceId, remote.contact.e164));
} else {
Log.w(TAG, "Bad serviceId on pinned contact! Length: " + remote.contact.serviceId);
return PinnedConversation.forEmpty();
}
} else if (remote.legacyGroupId != null && remote.legacyGroupId.size() > 0) {
return forGroupV1(remote.legacyGroupId.toByteArray());
} else if (remote.groupMasterKey != null && remote.groupMasterKey.size() > 0) {
return forGroupV2(remote.groupMasterKey.toByteArray());
} else {
return PinnedConversation.forEmpty();
}
}
public Optional<SignalServiceAddress> getContact() {
return contact;
}
public Optional<byte[]> getGroupV1Id() {
return groupV1Id;
}
public Optional<byte[]> getGroupV2MasterKey() {
return groupV2MasterKey;
}
public boolean isValid() {
return contact.isPresent() || groupV1Id.isPresent() || groupV2MasterKey.isPresent();
}
private AccountRecord.PinnedConversation toRemote() {
if (contact.isPresent()) {
AccountRecord.PinnedConversation.Contact.Builder contactBuilder = new AccountRecord.PinnedConversation.Contact.Builder();
contactBuilder.serviceId(contact.get().getServiceId().toString());
if (contact.get().getNumber().isPresent()) {
contactBuilder.e164(contact.get().getNumber().get());
}
return new AccountRecord.PinnedConversation.Builder().contact(contactBuilder.build()).build();
} else if (groupV1Id.isPresent()) {
return new AccountRecord.PinnedConversation.Builder().legacyGroupId(ByteString.of(groupV1Id.get())).build();
} else if (groupV2MasterKey.isPresent()) {
return new AccountRecord.PinnedConversation.Builder().groupMasterKey(ByteString.of(groupV2MasterKey.get())).build();
} else {
return new AccountRecord.PinnedConversation.Builder().build();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PinnedConversation that = (PinnedConversation) o;
return contact.equals(that.contact) &&
groupV1Id.equals(that.groupV1Id) &&
groupV2MasterKey.equals(that.groupV2MasterKey);
}
@Override
public int hashCode() {
return Objects.hash(contact, groupV1Id, groupV2MasterKey);
}
}
public static class Subscriber {
private final Optional<String> currencyCode;
private final Optional<byte[]> id;
public Subscriber(String currencyCode, byte[] id) {
if (currencyCode != null && id != null && id.length == 32) {
this.currencyCode = Optional.of(currencyCode);
this.id = Optional.of(id);
} else {
this.currencyCode = Optional.empty();
this.id = Optional.empty();
}
}
public Optional<String> getCurrencyCode() {
return currencyCode;
}
public Optional<byte[]> getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Subscriber that = (Subscriber) o;
return Objects.equals(currencyCode, that.currencyCode) && OptionalUtil.byteArrayEquals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(currencyCode, id);
}
}
public static class Payments {
private static final String TAG = Payments.class.getSimpleName();
private final boolean enabled;
private final Optional<byte[]> entropy;
public Payments(boolean enabled, Optional<byte[]> entropy) {
byte[] entropyBytes = entropy.orElse(null);
if (entropyBytes != null && entropyBytes.length != PaymentsConstants.PAYMENTS_ENTROPY_LENGTH) {
Log.w(TAG, "Blocked entropy of length " + entropyBytes.length);
entropyBytes = null;
}
this.entropy = Optional.ofNullable(entropyBytes);
this.enabled = enabled && this.entropy.isPresent();
}
public boolean isEnabled() {
return enabled;
}
public Optional<byte[]> getEntropy() {
return entropy;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Payments payments = (Payments) o;
return enabled == payments.enabled &&
OptionalUtil.byteArrayEquals(entropy, payments.entropy);
}
@Override
public int hashCode() {
return Objects.hash(enabled, entropy);
}
}
public static final class Builder {
private final StorageId id;
private final AccountRecord.Builder builder;
public Builder(byte[] rawId, byte[] serializedUnknowns) {
this.id = StorageId.forAccount(rawId);
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns);
} else {
this.builder = new AccountRecord.Builder();
}
}
public Builder setGivenName(String givenName) {
builder.givenName(givenName == null ? "" : givenName);
return this;
}
public Builder setFamilyName(String familyName) {
builder.familyName(familyName == null ? "" : familyName);
return this;
}
public Builder setProfileKey(byte[] profileKey) {
builder.profileKey(profileKey == null ? ByteString.EMPTY : ByteString.of(profileKey));
return this;
}
public Builder setAvatarUrlPath(String urlPath) {
builder.avatarUrlPath(urlPath == null ? "" : urlPath);
return this;
}
public Builder setNoteToSelfArchived(boolean archived) {
builder.noteToSelfArchived(archived);
return this;
}
public Builder setNoteToSelfForcedUnread(boolean forcedUnread) {
builder.noteToSelfMarkedUnread(forcedUnread);
return this;
}
public Builder setReadReceiptsEnabled(boolean enabled) {
builder.readReceipts(enabled);
return this;
}
public Builder setTypingIndicatorsEnabled(boolean enabled) {
builder.typingIndicators(enabled);
return this;
}
public Builder setSealedSenderIndicatorsEnabled(boolean enabled) {
builder.sealedSenderIndicators(enabled);
return this;
}
public Builder setLinkPreviewsEnabled(boolean enabled) {
builder.linkPreviews(enabled);
return this;
}
public Builder setPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode mode) {
builder.phoneNumberSharingMode(mode);
return this;
}
public Builder setUnlistedPhoneNumber(boolean unlisted) {
builder.unlistedPhoneNumber(unlisted);
return this;
}
public Builder setPinnedConversations(List<PinnedConversation> pinnedConversations) {
builder.pinnedConversations(pinnedConversations.stream().map(PinnedConversation::toRemote).collect(Collectors.toList()));
return this;
}
public Builder setPreferContactAvatars(boolean preferContactAvatars) {
builder.preferContactAvatars(preferContactAvatars);
return this;
}
public Builder setPayments(boolean enabled, byte[] entropy) {
org.whispersystems.signalservice.internal.storage.protos.Payments.Builder paymentsBuilder = new org.whispersystems.signalservice.internal.storage.protos.Payments.Builder();
boolean entropyPresent = entropy != null && entropy.length == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH;
paymentsBuilder.enabled(enabled && entropyPresent);
if (entropyPresent) {
paymentsBuilder.entropy(ByteString.of(entropy));
}
builder.payments(paymentsBuilder.build());
return this;
}
public Builder setUniversalExpireTimer(int timer) {
builder.universalExpireTimer(timer);
return this;
}
public Builder setPrimarySendsSms(boolean primarySendsSms) {
builder.primarySendsSms(primarySendsSms);
return this;
}
public Builder setE164(String e164) {
builder.e164(e164);
return this;
}
public Builder setDefaultReactions(List<String> defaultReactions) {
builder.preferredReactionEmoji(new ArrayList<>(defaultReactions));
return this;
}
public Builder setSubscriber(Subscriber subscriber) {
if (subscriber.id.isPresent() && subscriber.currencyCode.isPresent()) {
builder.subscriberId(ByteString.of(subscriber.id.get()));
builder.subscriberCurrencyCode(subscriber.currencyCode.get());
} else {
builder.subscriberId(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberId);
builder.subscriberCurrencyCode(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberCurrencyCode);
}
return this;
}
public Builder setBackupsSubscriber(Subscriber subscriber) {
if (subscriber.id.isPresent() && subscriber.currencyCode.isPresent()) {
builder.backupsSubscriberId(ByteString.of(subscriber.id.get()));
builder.backupsSubscriberCurrencyCode(subscriber.currencyCode.get());
} else {
builder.backupsSubscriberId(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberId);
builder.backupsSubscriberCurrencyCode(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberCurrencyCode);
}
return this;
}
public Builder setDisplayBadgesOnProfile(boolean displayBadgesOnProfile) {
builder.displayBadgesOnProfile(displayBadgesOnProfile);
return this;
}
public Builder setSubscriptionManuallyCancelled(boolean subscriptionManuallyCancelled) {
builder.subscriptionManuallyCancelled(subscriptionManuallyCancelled);
return this;
}
public Builder setKeepMutedChatsArchived(boolean keepMutedChatsArchived) {
builder.keepMutedChatsArchived(keepMutedChatsArchived);
return this;
}
public Builder setHasSetMyStoriesPrivacy(boolean hasSetMyStoriesPrivacy) {
builder.hasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy);
return this;
}
public Builder setHasViewedOnboardingStory(boolean hasViewedOnboardingStory) {
builder.hasViewedOnboardingStory(hasViewedOnboardingStory);
return this;
}
public Builder setStoriesDisabled(boolean storiesDisabled) {
builder.storiesDisabled(storiesDisabled);
return this;
}
public Builder setStoryViewReceiptsState(OptionalBool storyViewedReceiptsEnabled) {
builder.storyViewReceiptsEnabled(storyViewedReceiptsEnabled);
return this;
}
public Builder setHasSeenGroupStoryEducationSheet(boolean hasSeenGroupStoryEducationSheet) {
builder.hasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducationSheet);
return this;
}
public Builder setHasCompletedUsernameOnboarding(boolean hasCompletedUsernameOnboarding) {
builder.hasCompletedUsernameOnboarding(hasCompletedUsernameOnboarding);
return this;
}
public Builder setUsername(@Nullable String username) {
if (username == null || username.isEmpty()) {
builder.username(StorageRecordProtoUtil.getDefaultAccountRecord().username);
} else {
builder.username(username);
}
return this;
}
public Builder setUsernameLink(@Nullable AccountRecord.UsernameLink link) {
if (link == null) {
builder.usernameLink(StorageRecordProtoUtil.getDefaultAccountRecord().usernameLink);
} else {
builder.usernameLink(link);
}
return this;
}
private static AccountRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder();
} catch (IOException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
return new AccountRecord.Builder();
}
}
public SignalAccountRecord build() {
return new SignalAccountRecord(id, builder.build());
}
}
}

View file

@ -0,0 +1,59 @@
package org.whispersystems.signalservice.api.storage
import org.signal.core.util.hasUnknownFields
import org.signal.libsignal.protocol.logging.Log
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import java.io.IOException
class SignalAccountRecord(
override val id: StorageId,
override val proto: AccountRecord
) : SignalRecord<AccountRecord> {
companion object {
private val TAG: String = SignalAccountRecord::class.java.simpleName
fun newBuilder(serializedUnknowns: ByteArray?): AccountRecord.Builder {
return if (serializedUnknowns != null) {
parseUnknowns(serializedUnknowns)
} else {
AccountRecord.Builder()
}
}
private fun parseUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder {
try {
return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
Log.w(TAG, "Failed to combine unknown fields!", e)
return AccountRecord.Builder()
}
}
}
override fun asStorageRecord(): SignalStorageRecord {
return SignalStorageRecord.forAccount(this)
}
fun serializeUnknownFields(): ByteArray? {
return if (proto.hasUnknownFields()) proto.encode() else null
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SignalAccountRecord
if (id != other.id) return false
if (proto != other.proto) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + proto.hashCode()
return result
}
}

View file

@ -8,58 +8,23 @@ package org.whispersystems.signalservice.api.storage
import okio.ByteString.Companion.toByteString
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
import java.io.IOException
import java.util.LinkedList
/**
* A record in storage service that represents a call link that was already created.
*/
class SignalCallLinkRecord(private val id: StorageId, private val proto: CallLinkRecord) : SignalRecord {
class SignalCallLinkRecord(
override val id: StorageId,
override val proto: CallLinkRecord
) : SignalRecord<CallLinkRecord> {
val rootKey: ByteArray = proto.rootKey.toByteArray()
val adminPassKey: ByteArray = proto.adminPasskey.toByteArray()
val deletionTimestamp: Long = proto.deletedAtTimestampMs
fun toProto(): CallLinkRecord {
return proto
}
override fun getId(): StorageId {
return id
}
override fun asStorageRecord(): SignalStorageRecord {
return SignalStorageRecord.forCallLink(this)
}
override fun describeDiff(other: SignalRecord?): String {
return when (other) {
is SignalCallLinkRecord -> {
val diff = LinkedList<String>()
if (!rootKey.contentEquals(other.rootKey)) {
diff.add("RootKey")
}
if (!adminPassKey.contentEquals(other.adminPassKey)) {
diff.add("AdminPassKey")
}
if (deletionTimestamp != other.deletionTimestamp) {
diff.add("DeletionTimestamp")
}
diff.toString()
}
null -> {
"Other was null!"
}
else -> {
"Different class. ${this::class.java.getSimpleName()} | ${other::class.java.getSimpleName()}"
}
}
}
fun isDeleted(): Boolean {
return deletionTimestamp > 0
}

View file

@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -20,7 +21,7 @@ import javax.annotation.Nullable;
import okio.ByteString;
public final class SignalContactRecord implements SignalRecord {
public final class SignalContactRecord implements SignalRecord<ContactRecord> {
private static final String TAG = SignalContactRecord.class.getSimpleName();
@ -69,124 +70,13 @@ public final class SignalContactRecord implements SignalRecord {
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forContact(this);
public ContactRecord getProto() {
return proto;
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalContactRecord) {
SignalContactRecord that = (SignalContactRecord) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
diff.add("ID");
}
if (!Objects.equals(this.getAci(), that.getAci())) {
diff.add("ACI");
}
if (!Objects.equals(this.getPni(), that.getPni())) {
diff.add("PNI");
}
if (!Objects.equals(this.getNumber(), that.getNumber())) {
diff.add("E164");
}
if (!Objects.equals(this.profileGivenName, that.profileGivenName)) {
diff.add("ProfileGivenName");
}
if (!Objects.equals(this.profileFamilyName, that.profileFamilyName)) {
diff.add("ProfileFamilyName");
}
if (!Objects.equals(this.systemGivenName, that.systemGivenName)) {
diff.add("SystemGivenName");
}
if (!Objects.equals(this.systemFamilyName, that.systemFamilyName)) {
diff.add("SystemFamilyName");
}
if (!Objects.equals(this.systemNickname, that.systemNickname)) {
diff.add("SystemNickname");
}
if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) {
diff.add("ProfileKey");
}
if (!Objects.equals(this.username, that.username)) {
diff.add("Username");
}
if (!OptionalUtil.byteArrayEquals(this.identityKey, that.identityKey)) {
diff.add("IdentityKey");
}
if (!Objects.equals(this.getIdentityState(), that.getIdentityState())) {
diff.add("IdentityState");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
diff.add("MuteUntil");
}
if (shouldHideStory() != that.shouldHideStory()) {
diff.add("HideStory");
}
if (getUnregisteredTimestamp() != that.getUnregisteredTimestamp()) {
diff.add("UnregisteredTimestamp");
}
if (isHidden() != that.isHidden()) {
diff.add("Hidden");
}
if (isPniSignatureVerified() != that.isPniSignatureVerified()) {
diff.add("PniSignatureVerified");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
if (!Objects.equals(this.nicknameGivenName, that.nicknameGivenName)) {
diff.add("NicknameGivenName");
}
if (!Objects.equals(this.nicknameFamilyName, that.nicknameFamilyName)) {
diff.add("NicknameFamilyName");
}
if (!Objects.equals(this.note, that.note)) {
diff.add("Note");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forContact(this);
}
public boolean hasUnknownFields() {
@ -310,10 +200,6 @@ public final class SignalContactRecord implements SignalRecord {
return new SignalContactRecord(id, proto.newBuilder().pni("").build());
}
public ContactRecord toProto() {
return proto;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -12,7 +12,7 @@ import java.util.Objects;
import okio.ByteString;
public final class SignalGroupV1Record implements SignalRecord {
public final class SignalGroupV1Record implements SignalRecord<GroupV1Record> {
private static final String TAG = SignalGroupV1Record.class.getSimpleName();
@ -33,53 +33,13 @@ public final class SignalGroupV1Record implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV1(this);
@Override public GroupV1Record getProto() {
return proto;
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalGroupV1Record) {
SignalGroupV1Record that = (SignalGroupV1Record) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
diff.add("ID");
}
if (!Arrays.equals(this.groupId, that.groupId)) {
diff.add("MasterKey");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
diff.add("MuteUntil");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV1(this);
}
public boolean hasUnknownFields() {
@ -114,10 +74,6 @@ public final class SignalGroupV1Record implements SignalRecord {
return proto.mutedUntilTimestamp;
}
public GroupV1Record toProto() {
return proto;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
@ -14,7 +15,7 @@ import java.util.Objects;
import okio.ByteString;
public final class SignalGroupV2Record implements SignalRecord {
public final class SignalGroupV2Record implements SignalRecord<GroupV2Record> {
private static final String TAG = SignalGroupV2Record.class.getSimpleName();
@ -35,65 +36,13 @@ public final class SignalGroupV2Record implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV2(this);
@Override public GroupV2Record getProto() {
return proto;
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalGroupV2Record) {
SignalGroupV2Record that = (SignalGroupV2Record) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) {
diff.add("ID");
}
if (!Arrays.equals(this.getMasterKeyBytes(), that.getMasterKeyBytes())) {
diff.add("MasterKey");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
diff.add("MuteUntil");
}
if (!Objects.equals(this.notifyForMentionsWhenMuted(), that.notifyForMentionsWhenMuted())) {
diff.add("NotifyForMentionsWhenMuted");
}
if (shouldHideStory() != that.shouldHideStory()) {
diff.add("HideStory");
}
if (!Objects.equals(this.getStorySendMode(), that.getStorySendMode())) {
diff.add("StorySendMode");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV2(this);
}
public boolean hasUnknownFields() {
@ -148,10 +97,6 @@ public final class SignalGroupV2Record implements SignalRecord {
return proto.storySendMode;
}
public GroupV2Record toProto() {
return proto;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -1,7 +0,0 @@
package org.whispersystems.signalservice.api.storage;
public interface SignalRecord {
StorageId getId();
SignalStorageRecord asStorageRecord();
String describeDiff(SignalRecord other);
}

View file

@ -0,0 +1,52 @@
package org.whispersystems.signalservice.api.storage
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties
interface SignalRecord<E> {
val id: StorageId
val proto: E
fun asStorageRecord(): SignalStorageRecord
fun describeDiff(other: SignalRecord<*>): String {
if (this::class != other::class) {
return "Classes are different!"
}
if (this.proto!!::class != other.proto!!::class) {
return "Proto classes are different!"
}
val myFields = this.proto!!::class.memberProperties
val otherFields = other.proto!!::class.memberProperties
val myFieldsByName = myFields
.filter { it.isFinal && it.visibility == KVisibility.PUBLIC }
.associate { it.name to it.getter.call(this.proto!!) }
val otherFieldsByName = otherFields
.filter { it.isFinal && it.visibility == KVisibility.PUBLIC }
.associate { it.name to it.getter.call(other.proto!!) }
val mismatching = mutableListOf<String>()
if (this.id != other.id) {
mismatching += "id"
}
for (key in myFieldsByName.keys) {
val myValue = myFieldsByName[key]
val otherValue = otherFieldsByName[key]
if (myValue != otherValue) {
mismatching += key
}
}
return if (mismatching.isEmpty()) {
"All fields match."
} else {
mismatching.sorted().joinToString(prefix = "Some fields differ: ", separator = ", ")
}
}
}

View file

@ -64,17 +64,17 @@ public final class SignalStorageModels {
StorageRecord.Builder builder = new StorageRecord.Builder();
if (record.getContact().isPresent()) {
builder.contact(record.getContact().get().toProto());
builder.contact(record.getContact().get().getProto());
} else if (record.getGroupV1().isPresent()) {
builder.groupV1(record.getGroupV1().get().toProto());
builder.groupV1(record.getGroupV1().get().getProto());
} else if (record.getGroupV2().isPresent()) {
builder.groupV2(record.getGroupV2().get().toProto());
builder.groupV2(record.getGroupV2().get().getProto());
} else if (record.getAccount().isPresent()) {
builder.account(record.getAccount().get().toProto());
builder.account(record.getAccount().get().getProto());
} else if (record.getStoryDistributionList().isPresent()) {
builder.storyDistributionList(record.getStoryDistributionList().get().toProto());
builder.storyDistributionList(record.getStoryDistributionList().get().getProto());
} else if (record.getCallLink().isPresent()) {
builder.callLink(record.getCallLink().get().toProto());
builder.callLink(record.getCallLink().get().getProto());
} else {
throw new InvalidStorageWriteError();
}

View file

@ -7,7 +7,7 @@ import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord;
import java.util.Objects;
import java.util.Optional;
public class SignalStorageRecord implements SignalRecord {
public class SignalStorageRecord {
private final StorageId id;
private final Optional<SignalStoryDistributionListRecord> storyDistributionList;
@ -89,21 +89,10 @@ public class SignalStorageRecord implements SignalRecord {
this.callLink = callLink;
}
@Override
public StorageId getId() {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return this;
}
@Override
public String describeDiff(SignalRecord other) {
return "Diffs not supported.";
}
public int getType() {
return id.getType();
}

View file

@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.push.ServiceId;
@ -15,7 +16,7 @@ import java.util.stream.Collectors;
import okio.ByteString;
public class SignalStoryDistributionListRecord implements SignalRecord {
public class SignalStoryDistributionListRecord implements SignalRecord<StoryDistributionListRecord> {
private static final String TAG = SignalStoryDistributionListRecord.class.getSimpleName();
@ -42,12 +43,13 @@ public class SignalStoryDistributionListRecord implements SignalRecord {
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forStoryDistributionList(this);
public StoryDistributionListRecord getProto() {
return proto;
}
public StoryDistributionListRecord toProto() {
return proto;
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forStoryDistributionList(this);
}
public byte[] serializeUnknownFields() {
@ -78,46 +80,6 @@ public class SignalStoryDistributionListRecord implements SignalRecord {
return proto.isBlockList;
}
@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");
}
if (this.isBlockList() != that.isBlockList()) {
diff.add("BlockList");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;