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 oldRecord: SignalContactRecord = StorageSyncModels.localToRemoteRecord(SignalDatabase.recipients.getRecordForSync(harness.others[0])!!).contact.get()
val newProto = oldRecord val newProto = oldRecord
.toProto() .proto
.newBuilder() .newBuilder()
.identityState(ContactRecord.IdentityState.DEFAULT) .identityState(ContactRecord.IdentityState.DEFAULT)
.build() .build()

View File

@@ -17,8 +17,11 @@ import org.signal.core.util.SqlUtil
import org.signal.core.util.delete import org.signal.core.util.delete
import org.signal.core.util.exists import org.signal.core.util.exists
import org.signal.core.util.forEach 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.logging.Log
import org.signal.core.util.nullIfBlank import org.signal.core.util.nullIfBlank
import org.signal.core.util.nullIfEmpty
import org.signal.core.util.optionalString import org.signal.core.util.optionalString
import org.signal.core.util.or import org.signal.core.util.or
import org.signal.core.util.orNull import org.signal.core.util.orNull
@@ -1024,12 +1027,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
} }
fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate<SignalAccountRecord>) { fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate<SignalAccountRecord>) {
val profileName = ProfileName.fromParts(update.new.givenName.orElse(null), update.new.familyName.orElse(null)) val profileName = ProfileName.fromParts(update.new.proto.givenName, update.new.proto.familyName)
val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orElse(null)) val localKey = update.old.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) }
val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orElse(null)) val remoteKey = update.new.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) }
val profileKey: String? = remoteKey.or(localKey).map { obj: ProfileKey -> obj.serialize() }.map { source: ByteArray? -> Base64.encodeWithPadding(source!!) }.orElse(null) val profileKey: String? = (remoteKey ?: localKey)?.let { Base64.encodeWithPadding(it.serialize()) }
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"}.") 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 { 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!") 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)) put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.new.id.raw))
if (update.new.hasUnknownFields()) { if (update.new.proto.hasUnknownFields()) {
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(update.new.serializeUnknownFields()))) put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(update.new.serializeUnknownFields()!!))
} else { } else {
putNull(STORAGE_SERVICE_PROTO) putNull(STORAGE_SERVICE_PROTO)
} }
} }
if (update.new.username != null) { if (update.new.proto.username.nullIfBlank() != null) {
writableDatabase writableDatabase
.update(TABLE_NAME) .update(TABLE_NAME)
.values(USERNAME to null) .values(USERNAME to null)
.where("$USERNAME = ?", update.new.username!!) .where("$USERNAME = ?", update.new.proto.username)
.run() .run()
} }

View File

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

View File

@@ -124,9 +124,9 @@ public class StorageAccountRestoreJob extends BaseJob {
JobManager jobManager = AppDependencies.getJobManager(); JobManager jobManager = AppDependencies.getJobManager();
if (accountRecord.getAvatarUrlPath().isPresent()) { if (!accountRecord.getProto().avatarUrlPath.isEmpty()) {
Log.i(TAG, "Fetching avatar..."); 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()) { if (state.isPresent()) {
Log.i(TAG, "Avatar retrieved successfully. " + state.get()); 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]) * - Update the respective model (i.e. [SignalContactRecord])
* - Add getters * - Add getters
* - Update the builder * - Update the builder
* - Update [SignalRecord.describeDiff]. * - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make sure that you're:
* - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make * - Merging the attributes, likely preferring remote
* sure that you're: * - Adding to doParamsMatch()
* - Merging the attributes, likely preferring remote * - Adding the parameter to the builder chain when creating a merged model
* - Adding to doParamsMatch() * - Update builder usage in StorageSyncModels
* - 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 * - Handle the new data when writing to the local storage
* (i.e. [RecipientTable.applyStorageSyncContactUpdate]). * (i.e. [RecipientTable.applyStorageSyncContactUpdate]).
* - Make sure that whenever you change the field in the UI, we rotate the storageId for that row * - 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 android.preference.PreferenceManager
import org.signal.core.util.Base64 import org.signal.core.util.Base64
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.Curve import org.signal.libsignal.protocol.ecc.Curve
@@ -401,10 +402,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
var username: String? var username: String?
get() { get() {
val value = getString(KEY_USERNAME, null) val value = getString(KEY_USERNAME, null)
return if (value.isNullOrBlank()) null else value return value.nullIfBlank()
} }
set(value) { set(value) {
putString(KEY_USERNAME, value) putString(KEY_USERNAME, value.nullIfBlank())
} }
/** The local user's username link components, if set. */ /** The local user's username link components, if set. */

View File

@@ -1,14 +1,20 @@
package org.thoughtcrime.securesms.storage package org.thoughtcrime.securesms.storage
import android.content.Context 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.logging.Log
import org.signal.core.util.nullIfEmpty
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates
import org.thoughtcrime.securesms.storage.StorageSyncHelper.buildAccountRecord import org.thoughtcrime.securesms.storage.StorageSyncHelper.buildAccountRecord
import org.whispersystems.signalservice.api.storage.SignalAccountRecord import org.whispersystems.signalservice.api.storage.SignalAccountRecord
import org.whispersystems.signalservice.api.util.OptionalUtil import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord 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 org.whispersystems.signalservice.internal.storage.protos.OptionalBool
import java.util.Optional import java.util.Optional
@@ -45,184 +51,100 @@ class AccountRecordProcessor(
return false return false
} }
override fun getMatching(record: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional<SignalAccountRecord> { override fun getMatching(remote: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional<SignalAccountRecord> {
return Optional.of(localAccountRecord) return Optional.of(localAccountRecord)
} }
override fun merge(remote: SignalAccountRecord, local: SignalAccountRecord, keyGenerator: StorageKeyGenerator): SignalAccountRecord { override fun merge(remote: SignalAccountRecord, local: SignalAccountRecord, keyGenerator: StorageKeyGenerator): SignalAccountRecord {
val givenName: String val mergedGivenName: String
val familyName: String val mergedFamilyName: String
if (remote.givenName.isPresent || remote.familyName.isPresent) { if (remote.proto.givenName.isNotBlank() || remote.proto.familyName.isNotBlank()) {
givenName = remote.givenName.orElse("") mergedGivenName = remote.proto.givenName
familyName = remote.familyName.orElse("") mergedFamilyName = remote.proto.familyName
} else { } else {
givenName = local.givenName.orElse("") mergedGivenName = local.proto.givenName
familyName = local.familyName.orElse("") mergedFamilyName = local.proto.familyName
} }
val payments = if (remote.payments.entropy.isPresent) { val payments = if (remote.proto.payments?.entropy != null) {
remote.payments remote.proto.payments
} else { } else {
local.payments local.proto.payments
} }
val subscriber = if (remote.subscriber.id.isPresent) { val donationSubscriberId: ByteString
remote.subscriber val donationSubscriberCurrencyCode: String
if (remote.proto.subscriberId.isNotEmpty()) {
donationSubscriberId = remote.proto.subscriberId
donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode
} else { } else {
local.subscriber donationSubscriberId = local.proto.subscriberId
donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode
} }
val backupsSubscriber = if (remote.subscriber.id.isPresent) { val backupsSubscriberId: ByteString
remote.subscriber val backupsSubscriberCurrencyCode: String
if (remote.proto.backupsSubscriberId.isNotEmpty()) {
backupsSubscriberId = remote.proto.backupsSubscriberId
backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode
} else { } 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 { } else {
remote.storyViewReceiptsState remote.proto.storyViewReceiptsEnabled
} }
val unknownFields = remote.serializeUnknownFields() 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( val merged = SignalAccountRecord.newBuilder(unknownFields).apply {
contact = remote, givenName = mergedGivenName
unknownFields = unknownFields, familyName = mergedFamilyName
givenName = givenName, avatarUrlPath = remote.proto.avatarUrlPath.nullIfEmpty() ?: local.proto.avatarUrlPath
familyName = familyName, profileKey = remote.proto.profileKey.nullIfEmpty() ?: local.proto.profileKey
avatarUrlPath = avatarUrlPath, noteToSelfArchived = remote.proto.noteToSelfArchived
profileKey = profileKey, noteToSelfMarkedUnread = remote.proto.noteToSelfMarkedUnread
noteToSelfArchived = noteToSelfArchived, readReceipts = remote.proto.readReceipts
noteToSelfForcedUnread = noteToSelfForcedUnread, typingIndicators = remote.proto.typingIndicators
readReceipts = readReceipts, sealedSenderIndicators = remote.proto.sealedSenderIndicators
typingIndicators = typingIndicators, linkPreviews = remote.proto.linkPreviews
sealedSenderIndicators = sealedSenderIndicators, unlistedPhoneNumber = remote.proto.unlistedPhoneNumber
linkPreviewsEnabled = linkPreviews, pinnedConversations = remote.proto.pinnedConversations
phoneNumberSharingMode = phoneNumberSharingMode, phoneNumberSharingMode = remote.proto.phoneNumberSharingMode
unlistedPhoneNumber = unlisted, preferContactAvatars = remote.proto.preferContactAvatars
pinnedConversations = pinnedConversations, universalExpireTimer = remote.proto.universalExpireTimer
preferContactAvatars = preferContactAvatars, primarySendsSms = false
payments = payments, e164 = if (SignalStore.account.isPrimaryDevice) local.proto.e164 else remote.proto.e164
universalExpireTimer = universalExpireTimer, preferredReactionEmoji = remote.proto.preferredReactionEmoji.takeIf { it.isNotEmpty() } ?: local.proto.preferredReactionEmoji
primarySendsSms = primarySendsSms, displayBadgesOnProfile = remote.proto.displayBadgesOnProfile
e164 = e164, subscriptionManuallyCancelled = remote.proto.subscriptionManuallyCancelled
defaultReactions = defaultReactions, keepMutedChatsArchived = remote.proto.keepMutedChatsArchived
subscriber = subscriber, hasSetMyStoriesPrivacy = remote.proto.hasSetMyStoriesPrivacy
displayBadgesOnProfile = displayBadgesOnProfile, hasViewedOnboardingStory = remote.proto.hasViewedOnboardingStory || local.proto.hasViewedOnboardingStory
subscriptionManuallyCancelled = subscriptionManuallyCancelled, storiesDisabled = remote.proto.storiesDisabled
keepMutedChatsArchived = keepMutedChatsArchived, storyViewReceiptsEnabled = storyViewReceiptsState
hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy, hasSeenGroupStoryEducationSheet = remote.proto.hasSeenGroupStoryEducationSheet || local.proto.hasSeenGroupStoryEducationSheet
hasViewedOnboardingStory = hasViewedOnboardingStory, hasCompletedUsernameOnboarding = remote.proto.hasCompletedUsernameOnboarding || local.proto.hasCompletedUsernameOnboarding
hasCompletedUsernameOnboarding = hasSeenUsernameOnboarding, username = remote.proto.username
storiesDisabled = storiesDisabled, usernameLink = remote.proto.usernameLink
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
)
if (matchesRemote) { safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray())
return remote safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode)
} else if (matchesLocal) { safeSetBackupsSubscriber(backupsSubscriberId, backupsSubscriberCurrencyCode)
return local }.toSignalAccountRecord(StorageId.forAccount(keyGenerator.generate()))
return if (doParamsMatch(remote, merged)) {
remote
} else if (doParamsMatch(local, merged)) {
local
} else { } else {
val builder = SignalAccountRecord.Builder(keyGenerator.generate(), unknownFields) merged
.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()
} }
} }
@@ -238,72 +160,7 @@ class AccountRecordProcessor(
return 0 return 0
} }
private fun doParamsMatch( private fun doParamsMatch(base: SignalAccountRecord, test: SignalAccountRecord): Boolean {
contact: SignalAccountRecord, return base.serializeUnknownFields().contentEquals(test.serializeUnknownFields()) && base.proto == test.proto
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
} }
} }

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' * 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. * 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 { companion object {
private val TAG = Log.tag(DefaultStorageRecordProcessor::class.java) private val TAG = Log.tag(DefaultStorageRecordProcessor::class.java)
} }
@@ -37,16 +37,15 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordPr
@Throws(IOException::class) @Throws(IOException::class)
override fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator) { override fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator) {
val matchedRecords: MutableSet<E> = TreeSet(this) val matchedRecords: MutableSet<E> = TreeSet(this)
var i = 0
for (remote in remoteRecords) { for ((i, remote) in remoteRecords.withIndex()) {
if (isInvalid(remote)) { if (isInvalid(remote)) {
warn(i, remote, "Found invalid key! Ignoring it.") warn(i, remote, "Found invalid key! Ignoring it.")
} else { } else {
val local = getMatching(remote, keyGenerator) val local = getMatching(remote, keyGenerator)
if (local.isPresent) { if (local.isPresent) {
val merged = merge(remote, local.get(), keyGenerator) val merged: E = merge(remote, local.get(), keyGenerator)
if (matchedRecords.contains(local.get())) { if (matchedRecords.contains(local.get())) {
warn(i, remote, "Multiple remote records map to the same local record! Ignoring this one.") 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()) matchedRecords.add(local.get())
if (merged != remote) { if (merged != remote) {
info(i, remote, "[Remote Update] " + StorageRecordUpdate(remote, merged).toString()) info(i, remote, "[Remote Update] " + remote.describeDiff(merged))
} }
if (merged != local.get()) { if (merged != local.get()) {
@@ -68,8 +67,6 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord> : StorageRecordPr
insertLocal(remote) 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 * Handles processing a remote record, which involves applying any local changes that need to be
* made based on the remote records. * made based on the remote records.
*/ */
interface StorageRecordProcessor<E : SignalRecord?> { interface StorageRecordProcessor<E : SignalRecord<*>> {
@Throws(IOException::class) @Throws(IOException::class)
fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator) 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 android.content.Context
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import okio.ByteString
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64.encodeWithPadding import org.signal.core.util.Base64.encodeWithPadding
import org.signal.core.util.logging.Log 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.SignalStorageManifest
import org.whispersystems.signalservice.api.storage.SignalStorageRecord import org.whispersystems.signalservice.api.storage.SignalStorageRecord
import org.whispersystems.signalservice.api.storage.StorageId 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.OptionalUtil.byteArrayEquals
import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray import org.whispersystems.signalservice.api.util.toByteArray
@@ -130,52 +135,54 @@ object StorageSyncHelper {
val storageId = selfRecord?.storageId ?: self.storageId val storageId = selfRecord?.storageId ?: self.storageId
val account = SignalAccountRecord.Builder(storageId, selfRecord?.syncExtras?.storageProto) val accountRecord = SignalAccountRecord.newBuilder(selfRecord?.syncExtras?.storageProto).apply {
.setProfileKey(self.profileKey) profileKey = self.profileKey?.toByteString() ?: ByteString.EMPTY
.setGivenName(self.profileName.givenName) givenName = self.profileName.givenName
.setFamilyName(self.profileName.familyName) familyName = self.profileName.familyName
.setAvatarUrlPath(self.profileAvatar) avatarUrlPath = self.profileAvatar ?: ""
.setNoteToSelfArchived(selfRecord != null && selfRecord.syncExtras.isArchived) noteToSelfArchived = selfRecord != null && selfRecord.syncExtras.isArchived
.setNoteToSelfForcedUnread(selfRecord != null && selfRecord.syncExtras.isForcedUnread) noteToSelfMarkedUnread = selfRecord != null && selfRecord.syncExtras.isForcedUnread
.setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context)) typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context)
.setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)) readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context)
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)
.setLinkPreviewsEnabled(SignalStore.settings.isLinkPreviewsEnabled) linkPreviews = SignalStore.settings.isLinkPreviewsEnabled
.setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE) unlistedPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
.setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode)) phoneNumberSharingMode = StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode)
.setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned)) pinnedConversations = StorageSyncModels.localToRemotePinnedConversations(pinned)
.setPreferContactAvatars(SignalStore.settings.isPreferSystemContactPhotos) preferContactAvatars = SignalStore.settings.isPreferSystemContactPhotos
.setPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null)) primarySendsSms = false
.setPrimarySendsSms(false) universalExpireTimer = SignalStore.settings.universalExpireTimer
.setUniversalExpireTimer(SignalStore.settings.universalExpireTimer) preferredReactionEmoji = SignalStore.emoji.reactions
.setDefaultReactions(SignalStore.emoji.reactions) displayBadgesOnProfile = SignalStore.inAppPayments.getDisplayBadgesOnProfile()
.setSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION))) subscriptionManuallyCancelled = isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)
.setBackupsSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP))) keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived()
.setDisplayBadgesOnProfile(SignalStore.inAppPayments.getDisplayBadgesOnProfile()) hasSetMyStoriesPrivacy = SignalStore.story.userHasBeenNotifiedAboutStories
.setSubscriptionManuallyCancelled(isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)) hasViewedOnboardingStory = SignalStore.story.userHasViewedOnboardingStory
.setKeepMutedChatsArchived(SignalStore.settings.shouldKeepMutedChatsArchived()) storiesDisabled = SignalStore.story.isFeatureDisabled
.setHasSetMyStoriesPrivacy(SignalStore.story.userHasBeenNotifiedAboutStories) storyViewReceiptsEnabled = storyViewReceiptsState
.setHasViewedOnboardingStory(SignalStore.story.userHasViewedOnboardingStory) hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet
.setStoriesDisabled(SignalStore.story.isFeatureDisabled) hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding()
.setStoryViewReceiptsState(storyViewReceiptsState) username = SignalStore.account.username ?: ""
.setHasSeenGroupStoryEducationSheet(SignalStore.story.userHasSeenGroupStoryEducationSheet) usernameLink = SignalStore.account.usernameLink?.let { linkComponents ->
.setUsername(SignalStore.account.username) AccountRecord.UsernameLink(
.setHasCompletedUsernameOnboarding(SignalStore.uiHints.hasCompletedUsernameOnboarding()) entropy = linkComponents.entropy.toByteString(),
serverId = linkComponents.serverId.toByteArray().toByteString(),
color = StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)
)
}
val linkComponents = SignalStore.account.usernameLink getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let {
if (linkComponents != null) { safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
account.setUsernameLink( }
AccountRecord.UsernameLink.Builder()
.entropy(linkComponents.entropy.toByteString()) getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)?.let {
.serverId(linkComponents.serverId.toByteArray().toByteString()) safeSetBackupsSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
.color(StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)) }
.build()
) safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null))
} else {
account.setUsernameLink(null)
} }
return SignalStorageRecord.forAccount(account.build()) return SignalStorageRecord.forAccount(accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)))
} }
@JvmStatic @JvmStatic
@@ -188,62 +195,56 @@ object StorageSyncHelper {
fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, update: StorageRecordUpdate<SignalAccountRecord>, fetchProfile: Boolean) { fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, update: StorageRecordUpdate<SignalAccountRecord>, fetchProfile: Boolean) {
SignalDatabase.recipients.applyStorageSyncAccountUpdate(update) SignalDatabase.recipients.applyStorageSyncAccountUpdate(update)
TextSecurePreferences.setReadReceiptsEnabled(context, update.new.isReadReceiptsEnabled) TextSecurePreferences.setReadReceiptsEnabled(context, update.new.proto.readReceipts)
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.isTypingIndicatorsEnabled) TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.proto.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.isSealedSenderIndicatorsEnabled) TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.proto.sealedSenderIndicators)
SignalStore.settings.isLinkPreviewsEnabled = update.new.isLinkPreviewsEnabled SignalStore.settings.isLinkPreviewsEnabled = update.new.proto.linkPreviews
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.isPhoneNumberUnlisted) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.proto.unlistedPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.phoneNumberSharingMode) SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.proto.phoneNumberSharingMode)
SignalStore.settings.isPreferSystemContactPhotos = update.new.isPreferContactAvatars SignalStore.settings.isPreferSystemContactPhotos = update.new.proto.preferContactAvatars
SignalStore.payments.setEnabledAndEntropy(update.new.payments.isEnabled, Entropy.fromBytes(update.new.payments.entropy.orElse(null))) SignalStore.payments.setEnabledAndEntropy(update.new.proto.payments?.enabled == true, Entropy.fromBytes(update.new.proto.payments?.entropy?.toByteArray()))
SignalStore.settings.universalExpireTimer = update.new.universalExpireTimer SignalStore.settings.universalExpireTimer = update.new.proto.universalExpireTimer
SignalStore.emoji.reactions = update.new.defaultReactions SignalStore.emoji.reactions = update.new.proto.preferredReactionEmoji
SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.isDisplayBadgesOnProfile) SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.proto.displayBadgesOnProfile)
SignalStore.settings.setKeepMutedChatsArchived(update.new.isKeepMutedChatsArchived) SignalStore.settings.setKeepMutedChatsArchived(update.new.proto.keepMutedChatsArchived)
SignalStore.story.userHasBeenNotifiedAboutStories = update.new.hasSetMyStoriesPrivacy() SignalStore.story.userHasBeenNotifiedAboutStories = update.new.proto.hasSetMyStoriesPrivacy
SignalStore.story.userHasViewedOnboardingStory = update.new.hasViewedOnboardingStory() SignalStore.story.userHasViewedOnboardingStory = update.new.proto.hasViewedOnboardingStory
SignalStore.story.isFeatureDisabled = update.new.isStoriesDisabled SignalStore.story.isFeatureDisabled = update.new.proto.storiesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.hasSeenGroupStoryEducationSheet() SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.proto.hasSeenGroupStoryEducationSheet
SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.hasCompletedUsernameOnboarding()) SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.proto.hasCompletedUsernameOnboarding)
if (update.new.storyViewReceiptsState == OptionalBool.UNSET) { if (update.new.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) {
SignalStore.story.viewedReceiptsEnabled = update.new.isReadReceiptsEnabled SignalStore.story.viewedReceiptsEnabled = update.new.proto.readReceipts
} else { } else {
SignalStore.story.viewedReceiptsEnabled = update.new.storyViewReceiptsState == OptionalBool.ENABLED SignalStore.story.viewedReceiptsEnabled = update.new.proto.storyViewReceiptsEnabled == OptionalBool.ENABLED
} }
if (update.new.storyViewReceiptsState == OptionalBool.UNSET) { val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.proto.subscriberId, update.new.proto.subscriberCurrencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
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)
if (remoteSubscriber != null) { if (remoteSubscriber != null) {
setSubscriber(remoteSubscriber) setSubscriber(remoteSubscriber)
} }
if (update.new.isSubscriptionManuallyCancelled && !update.old.isSubscriptionManuallyCancelled) { if (update.new.proto.subscriptionManuallyCancelled && !update.old.proto.subscriptionManuallyCancelled) {
SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION) SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
} }
if (fetchProfile && update.new.avatarUrlPath.isPresent) { if (fetchProfile && update.new.proto.avatarUrlPath.isNotBlank()) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.avatarUrlPath.get())) AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.proto.avatarUrlPath))
} }
if (update.new.username != update.old.username) { if (update.new.proto.username != update.old.proto.username) {
SignalStore.account.username = update.new.username SignalStore.account.username = update.new.proto.username
SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account.usernameSyncErrorCount = 0 SignalStore.account.usernameSyncErrorCount = 0
} }
if (update.new.usernameLink != null) { if (update.new.proto.usernameLink != null) {
SignalStore.account.usernameLink = UsernameLinkComponents( SignalStore.account.usernameLink = UsernameLinkComponents(
update.new.usernameLink!!.entropy.toByteArray(), update.new.proto.usernameLink!!.entropy.toByteArray(),
UuidUtil.parseOrThrow(update.new.usernameLink!!.serverId.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 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.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState 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.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.SignalServiceAddress 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.SignalCallLinkRecord
import org.whispersystems.signalservice.api.storage.SignalContactRecord import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
@@ -82,18 +84,33 @@ object StorageSyncModels {
} }
@JvmStatic @JvmStatic
fun localToRemotePinnedConversations(records: List<RecipientRecord>): List<SignalAccountRecord.PinnedConversation> { fun localToRemotePinnedConversations(records: List<RecipientRecord>): List<AccountRecord.PinnedConversation> {
return records return records
.filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED } .filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED }
.map { localToRemotePinnedConversation(it) } .map { localToRemotePinnedConversation(it) }
} }
@JvmStatic @JvmStatic
private fun localToRemotePinnedConversation(settings: RecipientRecord): SignalAccountRecord.PinnedConversation { private fun localToRemotePinnedConversation(settings: RecipientRecord): AccountRecord.PinnedConversation {
return when (settings.recipientType) { return when (settings.recipientType) {
RecipientType.INDIVIDUAL -> SignalAccountRecord.PinnedConversation.forContact(SignalServiceAddress(settings.serviceId, settings.e164)) RecipientType.INDIVIDUAL -> {
RecipientType.GV1 -> SignalAccountRecord.PinnedConversation.forGroupV1(settings.groupId!!.requireV1().decodedId) AccountRecord.PinnedConversation(
RecipientType.GV2 -> SignalAccountRecord.PinnedConversation.forGroupV2(settings.syncExtras.groupMasterKey!!.serialize()) 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!") 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( fun remoteToLocalSubscriber(
subscriber: SignalAccountRecord.Subscriber, subscriberId: ByteString,
subscriberCurrencyCode: String,
type: InAppPaymentSubscriberRecord.Type type: InAppPaymentSubscriberRecord.Type
): InAppPaymentSubscriberRecord? { ): InAppPaymentSubscriberRecord? {
if (subscriber.id.isPresent) { if (subscriberId.isNotEmpty()) {
val subscriberId = SubscriberId.fromBytes(subscriber.id.get()) val subscriberId = SubscriberId.fromBytes(subscriberId.toByteArray())
val localSubscriberRecord = inAppPaymentSubscribers.getBySubscriberId(subscriberId) val localSubscriberRecord = inAppPaymentSubscribers.getBySubscriberId(subscriberId)
val requiresCancel = localSubscriberRecord != null && localSubscriberRecord.requiresCancel val requiresCancel = localSubscriberRecord != null && localSubscriberRecord.requiresCancel
val paymentMethodType = localSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN val paymentMethodType = localSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
val currency: Currency val currency: Currency
if (subscriber.currencyCode.isEmpty) { if (subscriberCurrencyCode.isBlank()) {
return null return null
} else { } else {
try { try {
currency = Currency.getInstance(subscriber.currencyCode.get()) currency = Currency.getInstance(subscriberCurrencyCode)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
return null 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!"); 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); .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); return new StorageRecordUpdate<>(oldRecord, newRecord);
} }

View File

@@ -32,6 +32,14 @@ fun ByteString?.isNullOrEmpty(): Boolean {
return this == null || this.size == 0 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. * 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 { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = signalKotlinJvmTarget 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 okio.ByteString.Companion.toByteString
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
import java.io.IOException import java.io.IOException
import java.util.LinkedList
/** /**
* A record in storage service that represents a call link that was already created. * 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 rootKey: ByteArray = proto.rootKey.toByteArray()
val adminPassKey: ByteArray = proto.adminPasskey.toByteArray() val adminPassKey: ByteArray = proto.adminPasskey.toByteArray()
val deletionTimestamp: Long = proto.deletedAtTimestampMs val deletionTimestamp: Long = proto.deletedAtTimestampMs
fun toProto(): CallLinkRecord {
return proto
}
override fun getId(): StorageId {
return id
}
override fun asStorageRecord(): SignalStorageRecord { override fun asStorageRecord(): SignalStorageRecord {
return SignalStorageRecord.forCallLink(this) 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 { fun isDeleted(): Boolean {
return deletionTimestamp > 0 return deletionTimestamp > 0
} }

View File

@@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.storage; package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil; import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
@@ -20,7 +21,7 @@ import javax.annotation.Nullable;
import okio.ByteString; import okio.ByteString;
public final class SignalContactRecord implements SignalRecord { public final class SignalContactRecord implements SignalRecord<ContactRecord> {
private static final String TAG = SignalContactRecord.class.getSimpleName(); private static final String TAG = SignalContactRecord.class.getSimpleName();
@@ -69,124 +70,13 @@ public final class SignalContactRecord implements SignalRecord {
} }
@Override @Override
public SignalStorageRecord asStorageRecord() { public ContactRecord getProto() {
return SignalStorageRecord.forContact(this); return proto;
} }
@Override @Override
public String describeDiff(SignalRecord other) { public SignalStorageRecord asStorageRecord() {
if (other instanceof SignalContactRecord) { return SignalStorageRecord.forContact(this);
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 boolean hasUnknownFields() { public boolean hasUnknownFields() {
@@ -310,10 +200,6 @@ public final class SignalContactRecord implements SignalRecord {
return new SignalContactRecord(id, proto.newBuilder().pni("").build()); return new SignalContactRecord(id, proto.newBuilder().pni("").build());
} }
public ContactRecord toProto() {
return proto;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -12,7 +12,7 @@ import java.util.Objects;
import okio.ByteString; import okio.ByteString;
public final class SignalGroupV1Record implements SignalRecord { public final class SignalGroupV1Record implements SignalRecord<GroupV1Record> {
private static final String TAG = SignalGroupV1Record.class.getSimpleName(); private static final String TAG = SignalGroupV1Record.class.getSimpleName();
@@ -33,53 +33,13 @@ public final class SignalGroupV1Record implements SignalRecord {
return id; return id;
} }
@Override @Override public GroupV1Record getProto() {
public SignalStorageRecord asStorageRecord() { return proto;
return SignalStorageRecord.forGroupV1(this);
} }
@Override @Override
public String describeDiff(SignalRecord other) { public SignalStorageRecord asStorageRecord() {
if (other instanceof SignalGroupV1Record) { return SignalStorageRecord.forGroupV1(this);
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 boolean hasUnknownFields() { public boolean hasUnknownFields() {
@@ -114,10 +74,6 @@ public final class SignalGroupV1Record implements SignalRecord {
return proto.mutedUntilTimestamp; return proto.mutedUntilTimestamp;
} }
public GroupV1Record toProto() {
return proto;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.storage; package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil; import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
@@ -14,7 +15,7 @@ import java.util.Objects;
import okio.ByteString; import okio.ByteString;
public final class SignalGroupV2Record implements SignalRecord { public final class SignalGroupV2Record implements SignalRecord<GroupV2Record> {
private static final String TAG = SignalGroupV2Record.class.getSimpleName(); private static final String TAG = SignalGroupV2Record.class.getSimpleName();
@@ -35,65 +36,13 @@ public final class SignalGroupV2Record implements SignalRecord {
return id; return id;
} }
@Override @Override public GroupV2Record getProto() {
public SignalStorageRecord asStorageRecord() { return proto;
return SignalStorageRecord.forGroupV2(this);
} }
@Override @Override
public String describeDiff(SignalRecord other) { public SignalStorageRecord asStorageRecord() {
if (other instanceof SignalGroupV2Record) { return SignalStorageRecord.forGroupV2(this);
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 boolean hasUnknownFields() { public boolean hasUnknownFields() {
@@ -148,10 +97,6 @@ public final class SignalGroupV2Record implements SignalRecord {
return proto.storySendMode; return proto.storySendMode;
} }
public GroupV2Record toProto() {
return proto;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; 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(); StorageRecord.Builder builder = new StorageRecord.Builder();
if (record.getContact().isPresent()) { if (record.getContact().isPresent()) {
builder.contact(record.getContact().get().toProto()); builder.contact(record.getContact().get().getProto());
} else if (record.getGroupV1().isPresent()) { } else if (record.getGroupV1().isPresent()) {
builder.groupV1(record.getGroupV1().get().toProto()); builder.groupV1(record.getGroupV1().get().getProto());
} else if (record.getGroupV2().isPresent()) { } else if (record.getGroupV2().isPresent()) {
builder.groupV2(record.getGroupV2().get().toProto()); builder.groupV2(record.getGroupV2().get().getProto());
} else if (record.getAccount().isPresent()) { } else if (record.getAccount().isPresent()) {
builder.account(record.getAccount().get().toProto()); builder.account(record.getAccount().get().getProto());
} else if (record.getStoryDistributionList().isPresent()) { } else if (record.getStoryDistributionList().isPresent()) {
builder.storyDistributionList(record.getStoryDistributionList().get().toProto()); builder.storyDistributionList(record.getStoryDistributionList().get().getProto());
} else if (record.getCallLink().isPresent()) { } else if (record.getCallLink().isPresent()) {
builder.callLink(record.getCallLink().get().toProto()); builder.callLink(record.getCallLink().get().getProto());
} else { } else {
throw new InvalidStorageWriteError(); throw new InvalidStorageWriteError();
} }

View File

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

View File

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