mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-26 14:09:58 +00:00
The rest of the storage service unwrapping.
This commit is contained in:
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.recipientServiceAddresses
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
@@ -552,7 +553,7 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
|
||||
}
|
||||
|
||||
fun getRecipientIdForSyncRecord(record: SignalStoryDistributionListRecord): RecipientId? {
|
||||
val uuid: UUID = requireNotNull(UuidUtil.parseOrNull(record.identifier)) { "Incoming record did not have a valid identifier." }
|
||||
val uuid: UUID = requireNotNull(UuidUtil.parseOrNull(record.proto.identifier)) { "Incoming record did not have a valid identifier." }
|
||||
val distributionId = DistributionId.from(uuid)
|
||||
|
||||
return readableDatabase.query(
|
||||
@@ -591,30 +592,30 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
|
||||
}
|
||||
|
||||
fun applyStorageSyncStoryDistributionListInsert(insert: SignalStoryDistributionListRecord) {
|
||||
val distributionId = DistributionId.from(UuidUtil.parseOrThrow(insert.identifier))
|
||||
val distributionId = DistributionId.from(UuidUtil.parseOrThrow(insert.proto.identifier))
|
||||
if (distributionId == DistributionId.MY_STORY) {
|
||||
throw AssertionError("Should never try to insert My Story")
|
||||
}
|
||||
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
insert.isBlockList && insert.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
insert.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
insert.proto.isBlockList && insert.proto.recipientServiceIds.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
insert.proto.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
createList(
|
||||
name = insert.name,
|
||||
members = insert.recipients.map(RecipientId::from),
|
||||
name = insert.proto.name,
|
||||
members = insert.proto.recipientServiceAddresses.map(RecipientId::from),
|
||||
distributionId = distributionId,
|
||||
allowsReplies = insert.allowsReplies(),
|
||||
deletionTimestamp = insert.deletedAtTimestamp,
|
||||
allowsReplies = insert.proto.allowsReplies,
|
||||
deletionTimestamp = insert.proto.deletedAtTimestamp,
|
||||
privacyMode = privacyMode,
|
||||
storageId = insert.id.raw
|
||||
)
|
||||
}
|
||||
|
||||
fun applyStorageSyncStoryDistributionListUpdate(update: StorageRecordUpdate<SignalStoryDistributionListRecord>) {
|
||||
val distributionId = DistributionId.from(UuidUtil.parseOrThrow(update.new.identifier))
|
||||
val distributionId = DistributionId.from(UuidUtil.parseOrThrow(update.new.proto.identifier))
|
||||
|
||||
val distributionListId: DistributionListId? = readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ID), "${ListTable.DISTRIBUTION_ID} = ?", SqlUtil.buildArgs(distributionId.toString()), null, null, null).use { cursor ->
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
@@ -632,26 +633,26 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
|
||||
val recipientId = getRecipientId(distributionListId)!!
|
||||
SignalDatabase.recipients.updateStorageId(recipientId, update.new.id.raw)
|
||||
|
||||
if (update.new.deletedAtTimestamp > 0L) {
|
||||
if (update.new.proto.deletedAtTimestamp > 0L) {
|
||||
if (distributionId == DistributionId.MY_STORY) {
|
||||
Log.w(TAG, "Refusing to delete My Story.")
|
||||
return
|
||||
}
|
||||
|
||||
deleteList(distributionListId, update.new.deletedAtTimestamp)
|
||||
deleteList(distributionListId, update.new.proto.deletedAtTimestamp)
|
||||
return
|
||||
}
|
||||
|
||||
val privacyMode: DistributionListPrivacyMode = when {
|
||||
update.new.isBlockList && update.new.recipients.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
update.new.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
update.new.proto.isBlockList && update.new.proto.recipientServiceIds.isEmpty() -> DistributionListPrivacyMode.ALL
|
||||
update.new.proto.isBlockList -> DistributionListPrivacyMode.ALL_EXCEPT
|
||||
else -> DistributionListPrivacyMode.ONLY_WITH
|
||||
}
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val listTableValues = contentValuesOf(
|
||||
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
|
||||
ListTable.NAME to update.new.name,
|
||||
ListTable.ALLOWS_REPLIES to update.new.proto.allowsReplies,
|
||||
ListTable.NAME to update.new.proto.name,
|
||||
ListTable.IS_UNKNOWN to false,
|
||||
ListTable.PRIVACY_MODE to privacyMode.serialize()
|
||||
)
|
||||
@@ -664,7 +665,7 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign
|
||||
)
|
||||
|
||||
val currentlyInDistributionList = getRawMembers(distributionListId, privacyMode).toSet()
|
||||
val shouldBeInDistributionList = update.new.recipients.map(RecipientId::from).toSet()
|
||||
val shouldBeInDistributionList = update.new.proto.recipientServiceAddresses.map(RecipientId::from).toSet()
|
||||
val toRemove = currentlyInDistributionList - shouldBeInDistributionList
|
||||
val toAdd = shouldBeInDistributionList - currentlyInDistributionList
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.signal.core.util.nullIfBlank
|
||||
import org.signal.core.util.nullIfEmpty
|
||||
import org.signal.core.util.optionalString
|
||||
import org.signal.core.util.or
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSet
|
||||
import org.signal.core.util.readToSingleBoolean
|
||||
@@ -43,6 +42,7 @@ import org.signal.core.util.updateAll
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.InvalidKeyException
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
@@ -113,12 +113,13 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.signalAci
|
||||
import org.whispersystems.signalservice.api.storage.signalPni
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
import java.util.LinkedList
|
||||
import java.util.Objects
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
@@ -861,7 +862,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
val recipientId: RecipientId
|
||||
if (id < 0) {
|
||||
Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.")
|
||||
recipientId = getAndPossiblyMerge(aci = insert.aci.orNull(), pni = insert.pni.orNull(), e164 = insert.number.orNull(), pniVerified = insert.isPniSignatureVerified)
|
||||
recipientId = getAndPossiblyMerge(aci = ACI.parseOrNull(insert.proto.aci), pni = PNI.parseOrNull(insert.proto.pni), e164 = insert.proto.e164.nullIfBlank(), pniVerified = insert.proto.pniSignatureVerified)
|
||||
resolvePotentialUsernameConflicts(values.getAsString(USERNAME), recipientId)
|
||||
|
||||
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId))
|
||||
@@ -869,18 +870,18 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
recipientId = RecipientId.from(id)
|
||||
}
|
||||
|
||||
if (insert.identityKey.isPresent && (insert.aci.isPresent || insert.pni.isPresent)) {
|
||||
if (insert.proto.identityKey.isNotEmpty() && (insert.proto.signalAci != null || insert.proto.signalPni != null)) {
|
||||
try {
|
||||
val serviceId: ServiceId = insert.aci.orNull() ?: insert.pni.get()
|
||||
val identityKey = IdentityKey(insert.identityKey.get(), 0)
|
||||
identities.updateIdentityAfterSync(serviceId.toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState))
|
||||
val serviceId: ServiceId = insert.proto.signalAci ?: insert.proto.signalPni!!
|
||||
val identityKey = IdentityKey(insert.proto.identityKey.toByteArray(), 0)
|
||||
identities.updateIdentityAfterSync(serviceId.toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.proto.identityState))
|
||||
} catch (e: InvalidKeyException) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e)
|
||||
}
|
||||
}
|
||||
|
||||
updateExtras(recipientId) {
|
||||
it.hideStory(insert.shouldHideStory())
|
||||
it.hideStory(insert.proto.hideStory)
|
||||
}
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, insert)
|
||||
@@ -901,7 +902,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
var recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.old.id.raw)).get()
|
||||
|
||||
Log.w(TAG, "[applyStorageSyncContactUpdate] Found user $recipientId. Possibly merging.")
|
||||
recipientId = getAndPossiblyMerge(aci = update.new.aci.orElse(null), pni = update.new.pni.orElse(null), e164 = update.new.number.orElse(null), pniVerified = update.new.isPniSignatureVerified)
|
||||
recipientId = getAndPossiblyMerge(aci = ACI.parseOrNull(update.new.proto.aci), pni = PNI.parseOrNull(update.new.proto.pni), e164 = update.new.proto.e164.nullIfBlank(), pniVerified = update.new.proto.pniSignatureVerified)
|
||||
|
||||
Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into $recipientId")
|
||||
resolvePotentialUsernameConflicts(values.getAsString(USERNAME), recipientId)
|
||||
@@ -919,9 +920,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
|
||||
try {
|
||||
val oldIdentityRecord = identityStore.getIdentityRecord(recipientId)
|
||||
if (update.new.identityKey.isPresent && update.new.aci.isPresent) {
|
||||
val identityKey = IdentityKey(update.new.identityKey.get(), 0)
|
||||
identities.updateIdentityAfterSync(update.new.aci.get().toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.new.identityState))
|
||||
if (update.new.proto.identityKey.isNotEmpty() && update.new.proto.signalAci != null) {
|
||||
val identityKey = IdentityKey(update.new.proto.identityKey.toByteArray(), 0)
|
||||
identities.updateIdentityAfterSync(update.new.proto.aci, recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.new.proto.identityState))
|
||||
}
|
||||
|
||||
val newIdentityRecord = identityStore.getIdentityRecord(recipientId)
|
||||
@@ -935,7 +936,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
|
||||
updateExtras(recipientId) {
|
||||
it.hideStory(update.new.shouldHideStory())
|
||||
it.hideStory(update.new.proto.hideStory)
|
||||
}
|
||||
|
||||
threads.applyStorageSyncUpdate(recipientId, update.new)
|
||||
@@ -968,13 +969,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
throw AssertionError("Had an update, but it didn't match any rows!")
|
||||
}
|
||||
|
||||
val recipient = Recipient.externalGroupExact(GroupId.v1orThrow(update.old.groupId))
|
||||
val recipient = Recipient.externalGroupExact(GroupId.v1orThrow(update.old.proto.id.toByteArray()))
|
||||
threads.applyStorageSyncUpdate(recipient.id, update.new)
|
||||
recipient.live().refresh()
|
||||
}
|
||||
|
||||
fun applyStorageSyncGroupV2Insert(insert: SignalGroupV2Record) {
|
||||
val masterKey = insert.masterKeyOrThrow
|
||||
val masterKey = GroupMasterKey(insert.proto.masterKey.toByteArray())
|
||||
val groupId = GroupId.v2(masterKey)
|
||||
val values = getValuesForStorageGroupV2(insert, true)
|
||||
|
||||
@@ -991,12 +992,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
Log.w(TAG, "Unable to create restore placeholder for $groupId, group already exists")
|
||||
}
|
||||
|
||||
groups.setShowAsStoryState(groupId, insert.storySendMode.toShowAsStoryState())
|
||||
groups.setShowAsStoryState(groupId, insert.proto.storySendMode.toShowAsStoryState())
|
||||
|
||||
val recipient = Recipient.externalGroupExact(groupId)
|
||||
|
||||
updateExtras(recipient.id) {
|
||||
it.hideStory(insert.shouldHideStory())
|
||||
it.hideStory(insert.proto.hideStory)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling request for latest group info for $groupId")
|
||||
@@ -1013,15 +1014,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
throw AssertionError("Had an update, but it didn't match any rows!")
|
||||
}
|
||||
|
||||
val masterKey = update.old.masterKeyOrThrow
|
||||
val masterKey = GroupMasterKey(update.old.proto.masterKey.toByteArray())
|
||||
val groupId = GroupId.v2(masterKey)
|
||||
val recipient = Recipient.externalGroupExact(groupId)
|
||||
|
||||
updateExtras(recipient.id) {
|
||||
it.hideStory(update.new.shouldHideStory())
|
||||
it.hideStory(update.new.proto.hideStory)
|
||||
}
|
||||
|
||||
groups.setShowAsStoryState(groupId, update.new.storySendMode.toShowAsStoryState())
|
||||
groups.setShowAsStoryState(groupId, update.new.proto.storySendMode.toShowAsStoryState())
|
||||
threads.applyStorageSyncUpdate(recipient.id, update.new)
|
||||
recipient.live().refresh()
|
||||
}
|
||||
@@ -1051,7 +1052,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.new.id.raw))
|
||||
|
||||
if (update.new.proto.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(update.new.serializeUnknownFields()!!))
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(update.new.serializedUnknowns!!))
|
||||
} else {
|
||||
putNull(STORAGE_SERVICE_PROTO)
|
||||
}
|
||||
@@ -4160,68 +4161,68 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
|
||||
private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
|
||||
return ContentValues().apply {
|
||||
val profileName = ProfileName.fromParts(contact.profileGivenName.orElse(null), contact.profileFamilyName.orElse(null))
|
||||
val systemName = ProfileName.fromParts(contact.systemGivenName.orElse(null), contact.systemFamilyName.orElse(null))
|
||||
val username = contact.username.orElse(null)
|
||||
val nickname = ProfileName.fromParts(contact.nicknameGivenName.orNull(), contact.nicknameFamilyName.orNull())
|
||||
val profileName = ProfileName.fromParts(contact.proto.givenName.nullIfBlank(), contact.proto.familyName.nullIfBlank())
|
||||
val systemName = ProfileName.fromParts(contact.proto.systemGivenName.nullIfBlank(), contact.proto.systemFamilyName.nullIfBlank())
|
||||
val username = contact.proto.username.nullIfBlank()
|
||||
val nickname = ProfileName.fromParts(contact.proto.nickname?.given, contact.proto.nickname?.family)
|
||||
|
||||
put(ACI_COLUMN, contact.aci.orElse(null)?.toString())
|
||||
put(PNI_COLUMN, contact.pni.orElse(null)?.toString())
|
||||
put(E164, contact.number.orElse(null))
|
||||
put(ACI_COLUMN, contact.proto.signalAci?.toString())
|
||||
put(PNI_COLUMN, contact.proto.signalPni?.toString())
|
||||
put(E164, contact.proto.e164.nullIfBlank())
|
||||
put(PROFILE_GIVEN_NAME, profileName.givenName)
|
||||
put(PROFILE_FAMILY_NAME, profileName.familyName)
|
||||
put(PROFILE_JOINED_NAME, profileName.toString())
|
||||
put(SYSTEM_GIVEN_NAME, systemName.givenName)
|
||||
put(SYSTEM_FAMILY_NAME, systemName.familyName)
|
||||
put(SYSTEM_JOINED_NAME, systemName.toString())
|
||||
put(SYSTEM_NICKNAME, contact.systemNickname.orElse(null))
|
||||
put(PROFILE_KEY, contact.profileKey.map { source -> Base64.encodeWithPadding(source) }.orElse(null))
|
||||
put(SYSTEM_NICKNAME, contact.proto.systemNickname.nullIfBlank())
|
||||
put(PROFILE_KEY, contact.proto.profileKey.takeIf { it.isNotEmpty() }?.let { source -> Base64.encodeWithPadding(source.toByteArray()) })
|
||||
put(USERNAME, if (TextUtils.isEmpty(username)) null else username)
|
||||
put(PROFILE_SHARING, if (contact.isProfileSharingEnabled) "1" else "0")
|
||||
put(BLOCKED, if (contact.isBlocked) "1" else "0")
|
||||
put(MUTE_UNTIL, contact.muteUntil)
|
||||
put(PROFILE_SHARING, contact.proto.whitelisted.toInt())
|
||||
put(BLOCKED, contact.proto.blocked.toInt())
|
||||
put(MUTE_UNTIL, contact.proto.mutedUntilTimestamp)
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(contact.id.raw))
|
||||
put(HIDDEN, contact.isHidden)
|
||||
put(PNI_SIGNATURE_VERIFIED, contact.isPniSignatureVerified.toInt())
|
||||
put(HIDDEN, contact.proto.hidden)
|
||||
put(PNI_SIGNATURE_VERIFIED, contact.proto.pniSignatureVerified.toInt())
|
||||
put(NICKNAME_GIVEN_NAME, nickname.givenName.nullIfBlank())
|
||||
put(NICKNAME_FAMILY_NAME, nickname.familyName.nullIfBlank())
|
||||
put(NICKNAME_JOINED_NAME, nickname.toString().nullIfBlank())
|
||||
put(NOTE, contact.note.orNull().nullIfBlank())
|
||||
put(NOTE, contact.proto.note.nullIfBlank())
|
||||
|
||||
if (contact.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(contact.serializeUnknownFields())))
|
||||
if (contact.proto.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(contact.serializedUnknowns!!))
|
||||
} else {
|
||||
putNull(STORAGE_SERVICE_PROTO)
|
||||
}
|
||||
|
||||
put(UNREGISTERED_TIMESTAMP, contact.unregisteredTimestamp)
|
||||
if (contact.unregisteredTimestamp > 0L) {
|
||||
put(UNREGISTERED_TIMESTAMP, contact.proto.unregisteredAtTimestamp)
|
||||
if (contact.proto.unregisteredAtTimestamp > 0L) {
|
||||
put(REGISTERED, RegisteredState.NOT_REGISTERED.id)
|
||||
} else if (contact.aci.isPresent) {
|
||||
} else if (contact.proto.signalAci != null) {
|
||||
put(REGISTERED, RegisteredState.REGISTERED.id)
|
||||
} else {
|
||||
Log.w(TAG, "Contact is marked as registered, but has no serviceId! Can't locally mark registered. (Phone: ${contact.number.orElse("null")}, Username: ${username?.isNotEmpty()})")
|
||||
Log.w(TAG, "Contact is marked as registered, but has no serviceId! Can't locally mark registered. (Phone: ${contact.proto.e164.nullIfBlank()}, Username: ${username?.isNotEmpty()})")
|
||||
}
|
||||
|
||||
if (isInsert) {
|
||||
put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.aci.map { it.toString() }.or(contact.pni.map { it.toString() }).orNull(), contact.number.orNull()).serialize())
|
||||
put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.proto.signalAci?.toString() ?: contact.proto.signalPni?.toString(), contact.proto.e164).serialize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getValuesForStorageGroupV1(groupV1: SignalGroupV1Record, isInsert: Boolean): ContentValues {
|
||||
return ContentValues().apply {
|
||||
val groupId = GroupId.v1orThrow(groupV1.groupId)
|
||||
val groupId = GroupId.v1orThrow(groupV1.proto.id.toByteArray())
|
||||
|
||||
put(GROUP_ID, groupId.toString())
|
||||
put(TYPE, RecipientType.GV1.id)
|
||||
put(PROFILE_SHARING, if (groupV1.isProfileSharingEnabled) "1" else "0")
|
||||
put(BLOCKED, if (groupV1.isBlocked) "1" else "0")
|
||||
put(MUTE_UNTIL, groupV1.muteUntil)
|
||||
put(PROFILE_SHARING, if (groupV1.proto.whitelisted) "1" else "0")
|
||||
put(BLOCKED, if (groupV1.proto.blocked) "1" else "0")
|
||||
put(MUTE_UNTIL, groupV1.proto.mutedUntilTimestamp)
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(groupV1.id.raw))
|
||||
|
||||
if (groupV1.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV1.serializeUnknownFields()))
|
||||
if (groupV1.proto.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV1.serializedUnknowns!!))
|
||||
} else {
|
||||
putNull(STORAGE_SERVICE_PROTO)
|
||||
}
|
||||
@@ -4234,18 +4235,18 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
|
||||
private fun getValuesForStorageGroupV2(groupV2: SignalGroupV2Record, isInsert: Boolean): ContentValues {
|
||||
return ContentValues().apply {
|
||||
val groupId = GroupId.v2(groupV2.masterKeyOrThrow)
|
||||
val groupId = GroupId.v2(GroupMasterKey(groupV2.proto.masterKey.toByteArray()))
|
||||
|
||||
put(GROUP_ID, groupId.toString())
|
||||
put(TYPE, RecipientType.GV2.id)
|
||||
put(PROFILE_SHARING, if (groupV2.isProfileSharingEnabled) "1" else "0")
|
||||
put(BLOCKED, if (groupV2.isBlocked) "1" else "0")
|
||||
put(MUTE_UNTIL, groupV2.muteUntil)
|
||||
put(PROFILE_SHARING, if (groupV2.proto.whitelisted) "1" else "0")
|
||||
put(BLOCKED, if (groupV2.proto.blocked) "1" else "0")
|
||||
put(MUTE_UNTIL, groupV2.proto.mutedUntilTimestamp)
|
||||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(groupV2.id.raw))
|
||||
put(MENTION_SETTING, if (groupV2.notifyForMentionsWhenMuted()) MentionSetting.ALWAYS_NOTIFY.id else MentionSetting.DO_NOT_NOTIFY.id)
|
||||
put(MENTION_SETTING, if (groupV2.proto.dontNotifyForMentionsIfMuted) MentionSetting.DO_NOT_NOTIFY.id else MentionSetting.ALWAYS_NOTIFY.id)
|
||||
|
||||
if (groupV2.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV2.serializeUnknownFields()))
|
||||
if (groupV2.proto.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV2.serializedUnknowns!!))
|
||||
} else {
|
||||
putNull(STORAGE_SERVICE_PROTO)
|
||||
}
|
||||
|
||||
@@ -1510,15 +1510,15 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
|
||||
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalContactRecord) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
|
||||
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
|
||||
}
|
||||
|
||||
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV1Record) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
|
||||
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
|
||||
}
|
||||
|
||||
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalGroupV2Record) {
|
||||
applyStorageSyncUpdate(recipientId, record.isArchived, record.isForcedUnread)
|
||||
applyStorageSyncUpdate(recipientId, record.proto.archived, record.proto.markedUnread)
|
||||
}
|
||||
|
||||
fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalAccountRecord) {
|
||||
|
||||
@@ -104,7 +104,7 @@ class AccountRecordProcessor(
|
||||
remote.proto.storyViewReceiptsEnabled
|
||||
}
|
||||
|
||||
val unknownFields = remote.serializeUnknownFields()
|
||||
val unknownFields = remote.serializedUnknowns
|
||||
|
||||
val merged = SignalAccountRecord.newBuilder(unknownFields).apply {
|
||||
givenName = mergedGivenName
|
||||
@@ -162,8 +162,4 @@ class AccountRecordProcessor(
|
||||
override fun compare(lhs: SignalAccountRecord, rhs: SignalAccountRecord): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun doParamsMatch(base: SignalAccountRecord, test: SignalAccountRecord): Boolean {
|
||||
return base.serializeUnknownFields().contentEquals(test.serializeUnknownFields()) && base.proto == test.proto
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,30 @@
|
||||
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.isNotEmpty
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.toSignalCallLinkRecord
|
||||
import java.util.Optional
|
||||
|
||||
internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkRecord>() {
|
||||
/**
|
||||
* Record processor for [SignalCallLinkRecord].
|
||||
* Handles merging and updating our local store when processing remote call link storage records.
|
||||
*/
|
||||
class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkRecord>() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallLinkRecordProcessor::class)
|
||||
}
|
||||
|
||||
override fun compare(o1: SignalCallLinkRecord?, o2: SignalCallLinkRecord?): Int {
|
||||
return if (o1?.rootKey.contentEquals(o2?.rootKey)) {
|
||||
return if (o1?.proto?.rootKey == o2?.proto?.rootKey) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
@@ -27,21 +36,21 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCal
|
||||
}
|
||||
|
||||
override fun isInvalid(remote: SignalCallLinkRecord): Boolean {
|
||||
return remote.adminPassKey.isNotEmpty() && remote.deletionTimestamp > 0L
|
||||
return remote.proto.adminPasskey.isNotEmpty() && remote.proto.deletedAtTimestampMs > 0L
|
||||
}
|
||||
|
||||
override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional<SignalCallLinkRecord> {
|
||||
Log.d(TAG, "Attempting to get matching record...")
|
||||
val rootKey = CallLinkRootKey(remote.rootKey)
|
||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey)
|
||||
val callRootKey = CallLinkRootKey(remote.proto.rootKey.toByteArray())
|
||||
val roomId = CallLinkRoomId.fromCallLinkRootKey(callRootKey)
|
||||
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(roomId)
|
||||
|
||||
if (callLink != null && callLink.credentials?.adminPassBytes != null) {
|
||||
val builder = SignalCallLinkRecord.Builder(keyGenerator.generate(), null).apply {
|
||||
setRootKey(rootKey.keyBytes)
|
||||
setAdminPassKey(callLink.credentials.adminPassBytes)
|
||||
setDeletedTimestamp(callLink.deletionTimestamp)
|
||||
}
|
||||
return Optional.of(builder.build())
|
||||
return SignalCallLinkRecord.newBuilder(null).apply {
|
||||
rootKey = callRootKey.keyBytes.toByteString()
|
||||
adminPasskey = callLink.credentials.adminPassBytes.toByteString()
|
||||
deletedAtTimestampMs = callLink.deletionTimestamp
|
||||
}.build().toSignalCallLinkRecord(StorageId.forCallLink(keyGenerator.generate())).toOptional()
|
||||
} else {
|
||||
return Optional.empty<SignalCallLinkRecord>()
|
||||
}
|
||||
@@ -53,15 +62,15 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCal
|
||||
* Other fields should not change, except for the clearing of the admin passkey on deletion
|
||||
*/
|
||||
override fun merge(remote: SignalCallLinkRecord, local: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): SignalCallLinkRecord {
|
||||
return if (remote.isDeleted() && local.isDeleted()) {
|
||||
if (remote.deletionTimestamp < local.deletionTimestamp) {
|
||||
return if (remote.proto.deletedAtTimestampMs > 0 && local.proto.deletedAtTimestampMs > 0) {
|
||||
if (remote.proto.deletedAtTimestampMs < local.proto.deletedAtTimestampMs) {
|
||||
remote
|
||||
} else {
|
||||
local
|
||||
}
|
||||
} else if (remote.isDeleted()) {
|
||||
} else if (remote.proto.deletedAtTimestampMs > 0) {
|
||||
remote
|
||||
} else if (local.isDeleted()) {
|
||||
} else if (local.proto.deletedAtTimestampMs > 0) {
|
||||
local
|
||||
} else {
|
||||
remote
|
||||
@@ -77,12 +86,12 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCal
|
||||
}
|
||||
|
||||
private fun insertOrUpdateRecord(record: SignalCallLinkRecord) {
|
||||
val rootKey = CallLinkRootKey(record.rootKey)
|
||||
val rootKey = CallLinkRootKey(record.proto.rootKey.toByteArray())
|
||||
|
||||
SignalDatabase.callLinks.insertOrUpdateCallLinkByRootKey(
|
||||
callLinkRootKey = rootKey,
|
||||
adminPassKey = record.adminPassKey,
|
||||
deletionTimestamp = record.deletionTimestamp,
|
||||
adminPassKey = record.proto.adminPasskey.toByteArray(),
|
||||
deletionTimestamp = record.proto.deletedAtTimestampMs,
|
||||
storageId = record.id
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
|
||||
|
||||
private static final String TAG = Log.tag(ContactRecordProcessor.class);
|
||||
|
||||
private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$");
|
||||
|
||||
private final RecipientTable recipientTable;
|
||||
|
||||
private final ACI selfAci;
|
||||
private final PNI selfPni;
|
||||
private final String selfE164;
|
||||
|
||||
public ContactRecordProcessor() {
|
||||
this(SignalStore.account().getAci(),
|
||||
SignalStore.account().getPni(),
|
||||
SignalStore.account().getE164(),
|
||||
SignalDatabase.recipients());
|
||||
}
|
||||
|
||||
ContactRecordProcessor(@Nullable ACI selfAci, @Nullable PNI selfPni, @Nullable String selfE164, @NonNull RecipientTable recipientTable) {
|
||||
this.recipientTable = recipientTable;
|
||||
this.selfAci = selfAci;
|
||||
this.selfPni = selfPni;
|
||||
this.selfE164 = selfE164;
|
||||
}
|
||||
|
||||
/**
|
||||
* For contact records specifically, we have some extra work that needs to be done before we process all of the records.
|
||||
*
|
||||
* We have to find all unregistered ACI-only records and split them into two separate contact rows locally, if necessary.
|
||||
* The reasons are nuanced, but the TL;DR is that we want to split unregistered users into separate rows so that a user
|
||||
* could re-register and get a different ACI.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void process(@NonNull Collection<? extends SignalContactRecord> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException {
|
||||
List<SignalContactRecord> unregisteredAciOnly = new ArrayList<>();
|
||||
|
||||
for (SignalContactRecord remoteRecord : remoteRecords) {
|
||||
if (isInvalid(remoteRecord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (remoteRecord.getUnregisteredTimestamp() > 0 && remoteRecord.getAci().isPresent() && remoteRecord.getPni().isEmpty() && remoteRecord.getNumber().isEmpty()) {
|
||||
unregisteredAciOnly.add(remoteRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if (unregisteredAciOnly.size() > 0) {
|
||||
for (SignalContactRecord aciOnly : unregisteredAciOnly) {
|
||||
SignalDatabase.recipients().splitForStorageSyncIfNecessary(aciOnly.getAci().get());
|
||||
}
|
||||
}
|
||||
|
||||
super.process(remoteRecords, keyGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error cases:
|
||||
* - You can't have a contact record without an ACI or PNI.
|
||||
* - You can't have a contact record for yourself. That should be an account record.
|
||||
*
|
||||
* Note: This method could be written more succinctly, but the logs are useful :)
|
||||
*/
|
||||
@Override
|
||||
public boolean isInvalid(@NonNull SignalContactRecord remote) {
|
||||
boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid();
|
||||
boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid();
|
||||
|
||||
if (!hasAci && !hasPni) {
|
||||
Log.w(TAG, "Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
|
||||
return true;
|
||||
} else if (selfAci != null && selfAci.equals(remote.getAci().orElse(null)) ||
|
||||
(selfPni != null && selfPni.equals(remote.getPni().orElse(null))) ||
|
||||
(selfE164 != null && remote.getNumber().isPresent() && remote.getNumber().get().equals(selfE164)))
|
||||
{
|
||||
Log.w(TAG, "Found a ContactRecord for ourselves -- marking as invalid.");
|
||||
return true;
|
||||
} else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) {
|
||||
Log.w(TAG, "Found a record with an invalid E164. Marking as invalid.");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<SignalContactRecord> getMatching(@NonNull SignalContactRecord remote, @NonNull StorageKeyGenerator keyGenerator) {
|
||||
Optional<RecipientId> found = remote.getAci().isPresent() ? recipientTable.getByAci(remote.getAci().get()) : Optional.empty();
|
||||
|
||||
if (found.isEmpty() && remote.getNumber().isPresent()) {
|
||||
found = recipientTable.getByE164(remote.getNumber().get());
|
||||
}
|
||||
|
||||
if (found.isEmpty() && remote.getPni().isPresent()) {
|
||||
found = recipientTable.getByPni(remote.getPni().get());
|
||||
}
|
||||
|
||||
return found.map(recipientTable::getRecordForSync)
|
||||
.map(settings -> {
|
||||
if (settings.getStorageId() != null) {
|
||||
return StorageSyncModels.localToRemoteRecord(settings);
|
||||
} else {
|
||||
Log.w(TAG, "Newly discovering a registered user via storage service. Saving a storageId for them.");
|
||||
recipientTable.updateStorageId(settings.getId(), keyGenerator.generate());
|
||||
|
||||
RecipientRecord updatedSettings = Objects.requireNonNull(recipientTable.getRecordForSync(settings.getId()));
|
||||
return StorageSyncModels.localToRemoteRecord(updatedSettings);
|
||||
}
|
||||
})
|
||||
.map(r -> new SignalContactRecord(r.getId(), r.getProto().contact));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) {
|
||||
String profileGivenName;
|
||||
String profileFamilyName;
|
||||
|
||||
if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) {
|
||||
profileGivenName = remote.getProfileGivenName().orElse("");
|
||||
profileFamilyName = remote.getProfileFamilyName().orElse("");
|
||||
} else {
|
||||
profileGivenName = local.getProfileGivenName().orElse("");
|
||||
profileFamilyName = local.getProfileFamilyName().orElse("");
|
||||
}
|
||||
|
||||
IdentityState identityState;
|
||||
byte[] identityKey;
|
||||
|
||||
if ((remote.getIdentityState() != local.getIdentityState() && remote.getIdentityKey().isPresent()) ||
|
||||
(remote.getIdentityKey().isPresent() && local.getIdentityKey().isEmpty()) ||
|
||||
(remote.getIdentityKey().isPresent() && local.getUnregisteredTimestamp() > 0))
|
||||
{
|
||||
identityState = remote.getIdentityState();
|
||||
identityKey = remote.getIdentityKey().get();
|
||||
} else {
|
||||
identityState = local.getIdentityState();
|
||||
identityKey = local.getIdentityKey().orElse(null);
|
||||
}
|
||||
|
||||
if (local.getAci().isPresent() && identityKey != null && remote.getIdentityKey().isPresent() && !Arrays.equals(identityKey, remote.getIdentityKey().get())) {
|
||||
Log.w(TAG, "The local and remote identity keys do not match for " + local.getAci().orElse(null) + ". Enqueueing a profile fetch.");
|
||||
RetrieveProfileJob.enqueue(Recipient.trustedPush(local.getAci().get(), local.getPni().orElse(null), local.getNumber().orElse(null)).getId());
|
||||
}
|
||||
|
||||
PNI pni;
|
||||
String e164;
|
||||
|
||||
boolean e164sMatchButPnisDont = local.getNumber().isPresent() &&
|
||||
local.getNumber().get().equals(remote.getNumber().orElse(null)) &&
|
||||
local.getPni().isPresent() &&
|
||||
remote.getPni().isPresent() &&
|
||||
!local.getPni().get().equals(remote.getPni().get());
|
||||
|
||||
boolean pnisMatchButE164sDont = local.getPni().isPresent() &&
|
||||
local.getPni().get().equals(remote.getPni().orElse(null)) &&
|
||||
local.getNumber().isPresent() &&
|
||||
remote.getNumber().isPresent() &&
|
||||
!local.getNumber().get().equals(remote.getNumber().get());
|
||||
|
||||
if (e164sMatchButPnisDont) {
|
||||
Log.w(TAG, "Matching E164s, but the PNIs differ! Trusting our local pair.");
|
||||
// TODO [pnp] Schedule CDS fetch?
|
||||
pni = local.getPni().get();
|
||||
e164 = local.getNumber().get();
|
||||
} else if (pnisMatchButE164sDont) {
|
||||
Log.w(TAG, "Matching PNIs, but the E164s differ! Trusting our local pair.");
|
||||
// TODO [pnp] Schedule CDS fetch?
|
||||
pni = local.getPni().get();
|
||||
e164 = local.getNumber().get();
|
||||
} else {
|
||||
pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null);
|
||||
e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null);
|
||||
}
|
||||
|
||||
byte[] unknownFields = remote.serializeUnknownFields();
|
||||
ACI aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get();
|
||||
byte[] profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
|
||||
String username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse("");
|
||||
boolean blocked = remote.isBlocked();
|
||||
boolean profileSharing = remote.isProfileSharingEnabled();
|
||||
boolean archived = remote.isArchived();
|
||||
boolean forcedUnread = remote.isForcedUnread();
|
||||
long muteUntil = remote.getMuteUntil();
|
||||
boolean hideStory = remote.shouldHideStory();
|
||||
long unregisteredTimestamp = remote.getUnregisteredTimestamp();
|
||||
boolean hidden = remote.isHidden();
|
||||
String systemGivenName = SignalStore.account().isPrimaryDevice() ? local.getSystemGivenName().orElse("") : remote.getSystemGivenName().orElse("");
|
||||
String systemFamilyName = SignalStore.account().isPrimaryDevice() ? local.getSystemFamilyName().orElse("") : remote.getSystemFamilyName().orElse("");
|
||||
String systemNickname = remote.getSystemNickname().orElse("");
|
||||
String nicknameGivenName = remote.getNicknameGivenName().orElse("");
|
||||
String nicknameFamilyName = remote.getNicknameFamilyName().orElse("");
|
||||
boolean pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified();
|
||||
String note = remote.getNote().or(local::getNote).orElse("");
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, aci, pni, e164, profileGivenName, profileFamilyName, systemGivenName, systemFamilyName, systemNickname, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden, pniSignatureVerified, nicknameGivenName, nicknameFamilyName, note);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, aci, pni, e164, profileGivenName, profileFamilyName, systemGivenName, systemFamilyName, systemNickname, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden, pniSignatureVerified, nicknameGivenName, nicknameFamilyName, note);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalContactRecord.Builder(keyGenerator.generate(), aci, unknownFields)
|
||||
.setE164(e164)
|
||||
.setPni(pni)
|
||||
.setProfileGivenName(profileGivenName)
|
||||
.setProfileFamilyName(profileFamilyName)
|
||||
.setSystemGivenName(systemGivenName)
|
||||
.setSystemFamilyName(systemFamilyName)
|
||||
.setSystemNickname(systemNickname)
|
||||
.setProfileKey(profileKey)
|
||||
.setUsername(username)
|
||||
.setIdentityState(identityState)
|
||||
.setIdentityKey(identityKey)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.setHideStory(hideStory)
|
||||
.setUnregisteredTimestamp(unregisteredTimestamp)
|
||||
.setHidden(hidden)
|
||||
.setPniSignatureVerified(pniSignatureVerified)
|
||||
.setNicknameGivenName(nicknameGivenName)
|
||||
.setNicknameFamilyName(nicknameFamilyName)
|
||||
.setNote(note)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertLocal(@NonNull SignalContactRecord record) {
|
||||
recipientTable.applyStorageSyncContactInsert(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLocal(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
|
||||
recipientTable.applyStorageSyncContactUpdate(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(@NonNull SignalContactRecord lhs, @NonNull SignalContactRecord rhs) {
|
||||
if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) ||
|
||||
(lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())) ||
|
||||
(lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni())))
|
||||
{
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidE164(String value) {
|
||||
return E164_PATTERN.matcher(value).matches();
|
||||
}
|
||||
|
||||
private static boolean doParamsMatch(@NonNull SignalContactRecord contact,
|
||||
@Nullable byte[] unknownFields,
|
||||
@Nullable ACI aci,
|
||||
@Nullable PNI pni,
|
||||
@Nullable String e164,
|
||||
@NonNull String profileGivenName,
|
||||
@NonNull String profileFamilyName,
|
||||
@NonNull String systemGivenName,
|
||||
@NonNull String systemFamilyName,
|
||||
@NonNull String systemNickname,
|
||||
@Nullable byte[] profileKey,
|
||||
@NonNull String username,
|
||||
@Nullable IdentityState identityState,
|
||||
@Nullable byte[] identityKey,
|
||||
boolean blocked,
|
||||
boolean profileSharing,
|
||||
boolean archived,
|
||||
boolean forcedUnread,
|
||||
long muteUntil,
|
||||
boolean hideStory,
|
||||
long unregisteredTimestamp,
|
||||
boolean hidden,
|
||||
boolean pniSignatureVerified,
|
||||
@NonNull String nicknameGivenName,
|
||||
@NonNull String nicknameFamilyName,
|
||||
@NonNull String note)
|
||||
{
|
||||
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
||||
Objects.equals(contact.getAci().orElse(null), aci) &&
|
||||
Objects.equals(contact.getPni().orElse(null), pni) &&
|
||||
Objects.equals(contact.getNumber().orElse(null), e164) &&
|
||||
Objects.equals(contact.getProfileGivenName().orElse(""), profileGivenName) &&
|
||||
Objects.equals(contact.getProfileFamilyName().orElse(""), profileFamilyName) &&
|
||||
Objects.equals(contact.getSystemGivenName().orElse(""), systemGivenName) &&
|
||||
Objects.equals(contact.getSystemFamilyName().orElse(""), systemFamilyName) &&
|
||||
Objects.equals(contact.getSystemNickname().orElse(""), systemNickname) &&
|
||||
Arrays.equals(contact.getProfileKey().orElse(null), profileKey) &&
|
||||
Objects.equals(contact.getUsername().orElse(""), username) &&
|
||||
Objects.equals(contact.getIdentityState(), identityState) &&
|
||||
Arrays.equals(contact.getIdentityKey().orElse(null), identityKey) &&
|
||||
contact.isBlocked() == blocked &&
|
||||
contact.isProfileSharingEnabled() == profileSharing &&
|
||||
contact.isArchived() == archived &&
|
||||
contact.isForcedUnread() == forcedUnread &&
|
||||
contact.getMuteUntil() == muteUntil &&
|
||||
contact.shouldHideStory() == hideStory &&
|
||||
contact.getUnregisteredTimestamp() == unregisteredTimestamp &&
|
||||
contact.isHidden() == hidden &&
|
||||
contact.isPniSignatureVerified() == pniSignatureVerified &&
|
||||
Objects.equals(contact.getNicknameGivenName().orElse(""), nicknameGivenName) &&
|
||||
Objects.equals(contact.getNicknameFamilyName().orElse(""), nicknameFamilyName) &&
|
||||
Objects.equals(contact.getNote().orElse(""), note);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.isEmpty
|
||||
import org.signal.core.util.isNotEmpty
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.nullIfBlank
|
||||
import org.signal.core.util.nullIfEmpty
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob.Companion.enqueue
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient.Companion.trustedPush
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels.localToRemoteRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.signalAci
|
||||
import org.whispersystems.signalservice.api.storage.signalPni
|
||||
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Record processor for [SignalContactRecord].
|
||||
* Handles merging and updating our local store when processing remote contact storage records.
|
||||
*/
|
||||
class ContactRecordProcessor(
|
||||
private val selfAci: ACI?,
|
||||
private val selfPni: PNI?,
|
||||
private val selfE164: String?,
|
||||
private val recipientTable: RecipientTable
|
||||
) : DefaultStorageRecordProcessor<SignalContactRecord>() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ContactRecordProcessor::class.java)
|
||||
|
||||
private val E164_PATTERN: Pattern = Pattern.compile("^\\+[1-9]\\d{0,18}$")
|
||||
|
||||
private fun isValidE164(value: String): Boolean {
|
||||
return E164_PATTERN.matcher(value).matches()
|
||||
}
|
||||
}
|
||||
|
||||
constructor() : this(
|
||||
selfAci = SignalStore.account.aci,
|
||||
selfPni = SignalStore.account.pni,
|
||||
selfE164 = SignalStore.account.e164,
|
||||
recipientTable = SignalDatabase.recipients
|
||||
)
|
||||
|
||||
/**
|
||||
* For contact records specifically, we have some extra work that needs to be done before we process all of the records.
|
||||
*
|
||||
* We have to find all unregistered ACI-only records and split them into two separate contact rows locally, if necessary.
|
||||
* The reasons are nuanced, but the TL;DR is that we want to split unregistered users into separate rows so that a user
|
||||
* could re-register and get a different ACI.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun process(remoteRecords: Collection<SignalContactRecord>, keyGenerator: StorageKeyGenerator) {
|
||||
val unregisteredAciOnly: MutableList<SignalContactRecord> = ArrayList()
|
||||
|
||||
for (remoteRecord in remoteRecords) {
|
||||
if (isInvalid(remoteRecord)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (remoteRecord.proto.unregisteredAtTimestamp > 0 && remoteRecord.proto.signalAci != null && remoteRecord.proto.signalPni == null && remoteRecord.proto.e164.isBlank()) {
|
||||
unregisteredAciOnly.add(remoteRecord)
|
||||
}
|
||||
}
|
||||
|
||||
if (unregisteredAciOnly.size > 0) {
|
||||
for (aciOnly in unregisteredAciOnly) {
|
||||
SignalDatabase.recipients.splitForStorageSyncIfNecessary(aciOnly.proto.signalAci!!)
|
||||
}
|
||||
}
|
||||
|
||||
super.process(remoteRecords, keyGenerator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error cases:
|
||||
* - You can't have a contact record without an ACI or PNI.
|
||||
* - You can't have a contact record for yourself. That should be an account record.
|
||||
*
|
||||
* Note: This method could be written more succinctly, but the logs are useful :)
|
||||
*/
|
||||
override fun isInvalid(remote: SignalContactRecord): Boolean {
|
||||
val hasAci = remote.proto.signalAci?.isValid == true
|
||||
val hasPni = remote.proto.signalPni?.isValid == true
|
||||
|
||||
if (!hasAci && !hasPni) {
|
||||
Log.w(TAG, "Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.")
|
||||
return true
|
||||
} else if (selfAci != null && selfAci == remote.proto.signalAci ||
|
||||
(selfPni != null && selfPni == remote.proto.signalPni) ||
|
||||
(selfE164 != null && remote.proto.e164.isNotBlank() && remote.proto.e164 == selfE164)
|
||||
) {
|
||||
Log.w(TAG, "Found a ContactRecord for ourselves -- marking as invalid.")
|
||||
return true
|
||||
} else if (remote.proto.e164.isNotBlank() && !isValidE164(remote.proto.e164)) {
|
||||
Log.w(TAG, "Found a record with an invalid E164. Marking as invalid.")
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMatching(remote: SignalContactRecord, keyGenerator: StorageKeyGenerator): Optional<SignalContactRecord> {
|
||||
var found: Optional<RecipientId> = remote.proto.signalAci?.let { recipientTable.getByAci(it) } ?: Optional.empty()
|
||||
|
||||
if (found.isEmpty && remote.proto.e164.isNotBlank()) {
|
||||
found = recipientTable.getByE164(remote.proto.e164)
|
||||
}
|
||||
|
||||
if (found.isEmpty && remote.proto.signalPni != null) {
|
||||
found = recipientTable.getByPni(remote.proto.signalPni!!)
|
||||
}
|
||||
|
||||
return found
|
||||
.map { recipientTable.getRecordForSync(it)!! }
|
||||
.map { settings: RecipientRecord ->
|
||||
if (settings.storageId != null) {
|
||||
return@map localToRemoteRecord(settings)
|
||||
} else {
|
||||
Log.w(TAG, "Newly discovering a registered user via storage service. Saving a storageId for them.")
|
||||
recipientTable.updateStorageId(settings.id, keyGenerator.generate())
|
||||
|
||||
val updatedSettings = recipientTable.getRecordForSync(settings.id)!!
|
||||
return@map localToRemoteRecord(updatedSettings)
|
||||
}
|
||||
}
|
||||
.map { record -> SignalContactRecord(record.id, record.proto.contact!!) }
|
||||
}
|
||||
|
||||
override fun merge(remote: SignalContactRecord, local: SignalContactRecord, keyGenerator: StorageKeyGenerator): SignalContactRecord {
|
||||
val mergedProfileGivenName: String
|
||||
val mergedProfileFamilyName: String
|
||||
|
||||
val localAci = local.proto.signalAci
|
||||
val localPni = local.proto.signalPni
|
||||
|
||||
val remoteAci = remote.proto.signalAci
|
||||
val remotePni = remote.proto.signalPni
|
||||
|
||||
if (remote.proto.givenName.isNotBlank() || remote.proto.familyName.isNotBlank()) {
|
||||
mergedProfileGivenName = remote.proto.givenName
|
||||
mergedProfileFamilyName = remote.proto.familyName
|
||||
} else {
|
||||
mergedProfileGivenName = local.proto.givenName
|
||||
mergedProfileFamilyName = local.proto.familyName
|
||||
}
|
||||
|
||||
val mergedIdentityState: IdentityState
|
||||
val mergedIdentityKey: ByteArray?
|
||||
|
||||
if ((remote.proto.identityState != local.proto.identityState && remote.proto.identityKey.isNotEmpty()) ||
|
||||
(remote.proto.identityKey.isNotEmpty() && local.proto.identityKey.isEmpty()) ||
|
||||
(remote.proto.identityKey.isNotEmpty() && local.proto.unregisteredAtTimestamp > 0)
|
||||
) {
|
||||
mergedIdentityState = remote.proto.identityState
|
||||
mergedIdentityKey = remote.proto.identityKey.takeIf { it.isNotEmpty() }?.toByteArray()
|
||||
} else {
|
||||
mergedIdentityState = local.proto.identityState
|
||||
mergedIdentityKey = local.proto.identityKey.takeIf { it.isNotEmpty() }?.toByteArray()
|
||||
}
|
||||
|
||||
if (localAci != null && mergedIdentityKey != null && remote.proto.identityKey.isNotEmpty() && !mergedIdentityKey.contentEquals(remote.proto.identityKey.toByteArray())) {
|
||||
Log.w(TAG, "The local and remote identity keys do not match for " + localAci + ". Enqueueing a profile fetch.")
|
||||
enqueue(trustedPush(localAci, localPni, local.proto.e164).id)
|
||||
}
|
||||
|
||||
val mergedPni: PNI?
|
||||
val mergedE164: String?
|
||||
|
||||
val e164sMatchButPnisDont = local.proto.e164.isNotBlank() &&
|
||||
local.proto.e164 == remote.proto.e164 &&
|
||||
localPni != null &&
|
||||
remotePni != null &&
|
||||
localPni != remotePni
|
||||
|
||||
val pnisMatchButE164sDont = localPni != null &&
|
||||
localPni == remotePni &&
|
||||
local.proto.e164.isNotBlank() &&
|
||||
remote.proto.e164.isNotBlank() &&
|
||||
local.proto.e164 != remote.proto.e164
|
||||
|
||||
if (e164sMatchButPnisDont) {
|
||||
Log.w(TAG, "Matching E164s, but the PNIs differ! Trusting our local pair.")
|
||||
// TODO [pnp] Schedule CDS fetch?
|
||||
mergedPni = localPni
|
||||
mergedE164 = local.proto.e164
|
||||
} else if (pnisMatchButE164sDont) {
|
||||
Log.w(TAG, "Matching PNIs, but the E164s differ! Trusting our local pair.")
|
||||
// TODO [pnp] Schedule CDS fetch?
|
||||
mergedPni = localPni
|
||||
mergedE164 = local.proto.e164
|
||||
} else {
|
||||
mergedPni = remotePni ?: localPni
|
||||
mergedE164 = remote.proto.e164.nullIfBlank() ?: local.proto.e164.nullIfBlank()
|
||||
}
|
||||
|
||||
val merged = SignalContactRecord.newBuilder(remote.serializedUnknowns).apply {
|
||||
e164 = mergedE164 ?: ""
|
||||
aci = local.proto.aci.nullIfBlank() ?: remote.proto.aci
|
||||
pni = mergedPni?.toStringWithoutPrefix() ?: ""
|
||||
givenName = mergedProfileGivenName
|
||||
familyName = mergedProfileFamilyName
|
||||
profileKey = remote.proto.profileKey.nullIfEmpty() ?: local.proto.profileKey
|
||||
username = remote.proto.username.nullIfBlank() ?: local.proto.username
|
||||
identityState = mergedIdentityState
|
||||
identityKey = mergedIdentityKey?.toByteString() ?: ByteString.EMPTY
|
||||
blocked = remote.proto.blocked
|
||||
whitelisted = remote.proto.whitelisted
|
||||
archived = remote.proto.archived
|
||||
markedUnread = remote.proto.markedUnread
|
||||
mutedUntilTimestamp = remote.proto.mutedUntilTimestamp
|
||||
hideStory = remote.proto.hideStory
|
||||
unregisteredAtTimestamp = remote.proto.unregisteredAtTimestamp
|
||||
hidden = remote.proto.hidden
|
||||
systemGivenName = if (SignalStore.account.isPrimaryDevice) local.proto.systemGivenName else remote.proto.systemGivenName
|
||||
systemFamilyName = if (SignalStore.account.isPrimaryDevice) local.proto.systemFamilyName else remote.proto.systemFamilyName
|
||||
systemNickname = remote.proto.systemNickname
|
||||
nickname = remote.proto.nickname
|
||||
pniSignatureVerified = remote.proto.pniSignatureVerified || local.proto.pniSignatureVerified
|
||||
note = remote.proto.note.nullIfBlank() ?: local.proto.note
|
||||
}.build().toSignalContactRecord(StorageId.forContact(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
val matchesLocal = doParamsMatch(local, merged)
|
||||
|
||||
return if (matchesRemote) {
|
||||
remote
|
||||
} else if (matchesLocal) {
|
||||
local
|
||||
} else {
|
||||
merged
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertLocal(record: SignalContactRecord) {
|
||||
recipientTable.applyStorageSyncContactInsert(record)
|
||||
}
|
||||
|
||||
override fun updateLocal(update: StorageRecordUpdate<SignalContactRecord>) {
|
||||
recipientTable.applyStorageSyncContactUpdate(update)
|
||||
}
|
||||
|
||||
override fun compare(lhs: SignalContactRecord, rhs: SignalContactRecord): Int {
|
||||
return if (
|
||||
(lhs.proto.signalAci != null && lhs.proto.aci == rhs.proto.aci) ||
|
||||
(lhs.proto.e164.isNotBlank() && lhs.proto.e164 == rhs.proto.e164) ||
|
||||
(lhs.proto.signalPni != null && lhs.proto.pni == rhs.proto.pni)
|
||||
) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,10 @@ abstract class DefaultStorageRecordProcessor<E : SignalRecord<*>> : StorageRecor
|
||||
}
|
||||
}
|
||||
|
||||
fun doParamsMatch(base: E, test: E): Boolean {
|
||||
return base.serializedUnknowns.contentEquals(test.serializedUnknowns) && base.proto == test.proto
|
||||
}
|
||||
|
||||
private fun info(i: Int, record: E, message: String) {
|
||||
Log.i(TAG, "[$i][${record.javaClass.getSimpleName()}] $message")
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Handles merging remote storage updates into local group v1 state.
|
||||
* Record processor for [SignalGroupV1Record].
|
||||
* Handles merging and updating our local store when processing remote gv1 storage records.
|
||||
*/
|
||||
class GroupV1RecordProcessor(private val groupDatabase: GroupTable, private val recipientTable: RecipientTable) : DefaultStorageRecordProcessor<SignalGroupV1Record>() {
|
||||
companion object {
|
||||
@@ -31,7 +33,7 @@ class GroupV1RecordProcessor(private val groupDatabase: GroupTable, private val
|
||||
*/
|
||||
override fun isInvalid(remote: SignalGroupV1Record): Boolean {
|
||||
try {
|
||||
val id = GroupId.v1(remote.groupId)
|
||||
val id = GroupId.v1(remote.proto.id.toByteArray())
|
||||
val v2Record = groupDatabase.getGroup(id.deriveV2MigrationGroupId())
|
||||
|
||||
if (v2Record.isPresent) {
|
||||
@@ -47,7 +49,7 @@ class GroupV1RecordProcessor(private val groupDatabase: GroupTable, private val
|
||||
}
|
||||
|
||||
override fun getMatching(remote: SignalGroupV1Record, keyGenerator: StorageKeyGenerator): Optional<SignalGroupV1Record> {
|
||||
val groupId = GroupId.v1orThrow(remote.groupId)
|
||||
val groupId = GroupId.v1orThrow(remote.proto.id.toByteArray())
|
||||
|
||||
val recipientId = recipientTable.getByGroupId(groupId)
|
||||
|
||||
@@ -58,28 +60,24 @@ class GroupV1RecordProcessor(private val groupDatabase: GroupTable, private val
|
||||
}
|
||||
|
||||
override fun merge(remote: SignalGroupV1Record, local: SignalGroupV1Record, keyGenerator: StorageKeyGenerator): SignalGroupV1Record {
|
||||
val unknownFields = remote.serializeUnknownFields()
|
||||
val blocked = remote.isBlocked
|
||||
val profileSharing = remote.isProfileSharingEnabled
|
||||
val archived = remote.isArchived
|
||||
val forcedUnread = remote.isForcedUnread
|
||||
val muteUntil = remote.muteUntil
|
||||
val merged = SignalGroupV1Record.newBuilder(remote.serializedUnknowns).apply {
|
||||
id = remote.proto.id
|
||||
blocked = remote.proto.blocked
|
||||
whitelisted = remote.proto.whitelisted
|
||||
archived = remote.proto.archived
|
||||
markedUnread = remote.proto.markedUnread
|
||||
mutedUntilTimestamp = remote.proto.mutedUntilTimestamp
|
||||
}.build().toSignalGroupV1Record(StorageId.forGroupV1(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(group = remote, unknownFields = unknownFields, blocked = blocked, profileSharing = profileSharing, archived = archived, forcedUnread = forcedUnread, muteUntil = muteUntil)
|
||||
val matchesLocal = doParamsMatch(group = local, unknownFields = unknownFields, blocked = blocked, profileSharing = profileSharing, archived = archived, forcedUnread = forcedUnread, muteUntil = muteUntil)
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
val matchesLocal = doParamsMatch(local, merged)
|
||||
|
||||
return if (matchesRemote) {
|
||||
remote
|
||||
} else if (matchesLocal) {
|
||||
local
|
||||
} else {
|
||||
SignalGroupV1Record.Builder(keyGenerator.generate(), remote.groupId, unknownFields)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.build()
|
||||
merged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,27 +90,10 @@ class GroupV1RecordProcessor(private val groupDatabase: GroupTable, private val
|
||||
}
|
||||
|
||||
override fun compare(lhs: SignalGroupV1Record, rhs: SignalGroupV1Record): Int {
|
||||
return if (lhs.groupId.contentEquals(rhs.groupId)) {
|
||||
return if (lhs.proto.id == rhs.proto.id) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
private fun doParamsMatch(
|
||||
group: SignalGroupV1Record,
|
||||
unknownFields: ByteArray?,
|
||||
blocked: Boolean,
|
||||
profileSharing: Boolean,
|
||||
archived: Boolean,
|
||||
forcedUnread: Boolean,
|
||||
muteUntil: Long
|
||||
): Boolean {
|
||||
return unknownFields.contentEquals(group.serializeUnknownFields()) &&
|
||||
blocked == group.isBlocked &&
|
||||
profileSharing == group.isProfileSharingEnabled &&
|
||||
archived == group.isArchived &&
|
||||
forcedUnread == group.isForcedUnread &&
|
||||
muteUntil == group.muteUntil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Record processor for [SignalGroupV2Record].
|
||||
* Handles merging and updating our local store when processing remote gv2 storage records.
|
||||
*/
|
||||
class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private val groupDatabase: GroupTable) : DefaultStorageRecordProcessor<SignalGroupV2Record>() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(GroupV2RecordProcessor::class.java)
|
||||
@@ -21,11 +25,11 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private
|
||||
constructor() : this(SignalDatabase.recipients, SignalDatabase.groups)
|
||||
|
||||
override fun isInvalid(remote: SignalGroupV2Record): Boolean {
|
||||
return remote.masterKeyBytes.size != GroupMasterKey.SIZE
|
||||
return remote.proto.masterKey.size != GroupMasterKey.SIZE
|
||||
}
|
||||
|
||||
override fun getMatching(remote: SignalGroupV2Record, keyGenerator: StorageKeyGenerator): Optional<SignalGroupV2Record> {
|
||||
val groupId = GroupId.v2(remote.masterKeyOrThrow)
|
||||
val groupId = GroupId.v2(GroupMasterKey(remote.proto.masterKey.toByteArray()))
|
||||
|
||||
val recipientId = recipientTable.getByGroupId(groupId)
|
||||
|
||||
@@ -36,64 +40,35 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private
|
||||
StorageSyncModels.localToRemoteRecord(settings)
|
||||
} else {
|
||||
Log.w(TAG, "No local master key. Assuming it matches remote since the groupIds match. Enqueuing a fetch to fix the bad state.")
|
||||
groupDatabase.fixMissingMasterKey(remote.masterKeyOrThrow)
|
||||
StorageSyncModels.localToRemoteRecord(settings, remote.masterKeyOrThrow)
|
||||
groupDatabase.fixMissingMasterKey(GroupMasterKey(remote.proto.masterKey.toByteArray()))
|
||||
StorageSyncModels.localToRemoteRecord(settings, GroupMasterKey(remote.proto.masterKey.toByteArray()))
|
||||
}
|
||||
}
|
||||
.map { record: SignalStorageRecord -> record.proto.groupV2!!.toSignalGroupV2Record(record.id) }
|
||||
}
|
||||
|
||||
override fun merge(remote: SignalGroupV2Record, local: SignalGroupV2Record, keyGenerator: StorageKeyGenerator): SignalGroupV2Record {
|
||||
val unknownFields = remote.serializeUnknownFields()
|
||||
val blocked = remote.isBlocked
|
||||
val profileSharing = remote.isProfileSharingEnabled
|
||||
val archived = remote.isArchived
|
||||
val forcedUnread = remote.isForcedUnread
|
||||
val muteUntil = remote.muteUntil
|
||||
val notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted()
|
||||
val hideStory = remote.shouldHideStory()
|
||||
val storySendMode = remote.storySendMode
|
||||
val merged = SignalGroupV2Record.newBuilder(remote.serializedUnknowns).apply {
|
||||
masterKey = remote.proto.masterKey
|
||||
blocked = remote.proto.blocked
|
||||
whitelisted = remote.proto.whitelisted
|
||||
archived = remote.proto.archived
|
||||
markedUnread = remote.proto.markedUnread
|
||||
mutedUntilTimestamp = remote.proto.mutedUntilTimestamp
|
||||
dontNotifyForMentionsIfMuted = remote.proto.dontNotifyForMentionsIfMuted
|
||||
hideStory = remote.proto.hideStory
|
||||
storySendMode = remote.proto.storySendMode
|
||||
}.build().toSignalGroupV2Record(StorageId.forGroupV2(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(
|
||||
group = remote,
|
||||
unknownFields = unknownFields,
|
||||
blocked = blocked,
|
||||
profileSharing = profileSharing,
|
||||
archived = archived,
|
||||
forcedUnread = forcedUnread,
|
||||
muteUntil = muteUntil,
|
||||
notifyForMentionsWhenMuted = notifyForMentionsWhenMuted,
|
||||
hideStory = hideStory,
|
||||
storySendMode = storySendMode
|
||||
)
|
||||
val matchesLocal = doParamsMatch(
|
||||
group = local,
|
||||
unknownFields = unknownFields,
|
||||
blocked = blocked,
|
||||
profileSharing = profileSharing,
|
||||
archived = archived,
|
||||
forcedUnread = forcedUnread,
|
||||
muteUntil = muteUntil,
|
||||
notifyForMentionsWhenMuted = notifyForMentionsWhenMuted,
|
||||
hideStory = hideStory,
|
||||
storySendMode = storySendMode
|
||||
)
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
val matchesLocal = doParamsMatch(local, merged)
|
||||
|
||||
return if (matchesRemote) {
|
||||
remote
|
||||
} else if (matchesLocal) {
|
||||
local
|
||||
} else {
|
||||
SignalGroupV2Record.Builder(keyGenerator.generate(), remote.masterKeyBytes, unknownFields)
|
||||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted)
|
||||
.setHideStory(hideStory)
|
||||
.setStorySendMode(storySendMode)
|
||||
.build()
|
||||
merged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,33 +81,10 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private
|
||||
}
|
||||
|
||||
override fun compare(lhs: SignalGroupV2Record, rhs: SignalGroupV2Record): Int {
|
||||
return if (lhs.masterKeyBytes.contentEquals(rhs.masterKeyBytes)) {
|
||||
return if (lhs.proto.masterKey == rhs.proto.masterKey) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
private fun doParamsMatch(
|
||||
group: SignalGroupV2Record,
|
||||
unknownFields: ByteArray?,
|
||||
blocked: Boolean,
|
||||
profileSharing: Boolean,
|
||||
archived: Boolean,
|
||||
forcedUnread: Boolean,
|
||||
muteUntil: Long,
|
||||
notifyForMentionsWhenMuted: Boolean,
|
||||
hideStory: Boolean,
|
||||
storySendMode: GroupV2Record.StorySendMode
|
||||
): Boolean {
|
||||
return unknownFields.contentEquals(group.serializeUnknownFields()) &&
|
||||
blocked == group.isBlocked &&
|
||||
profileSharing == group.isProfileSharingEnabled &&
|
||||
archived == group.isArchived &&
|
||||
forcedUnread == group.isForcedUnread &&
|
||||
muteUntil == group.muteUntil &&
|
||||
notifyForMentionsWhenMuted == group.notifyForMentionsWhenMuted() &&
|
||||
hideStory == group.shouldHideStory() &&
|
||||
storySendMode == group.storySendMode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ 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.storage.toSignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil.byteArrayEquals
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
@@ -105,7 +104,7 @@ object StorageSyncHelper {
|
||||
|
||||
@JvmStatic
|
||||
fun profileKeyChanged(update: StorageRecordUpdate<SignalContactRecord>): Boolean {
|
||||
return !byteArrayEquals(update.old.profileKey, update.new.profileKey)
|
||||
return update.old.proto.profileKey != update.new.proto.profileKey
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -18,17 +18,23 @@ import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.toSignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
||||
import org.whispersystems.signalservice.api.storage.toSignalStorageRecord
|
||||
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||
import java.util.Currency
|
||||
@@ -150,33 +156,31 @@ object StorageSyncModels {
|
||||
throw AssertionError("Must have either a UUID or a phone number!")
|
||||
}
|
||||
|
||||
val hideStory = recipient.extras != null && recipient.extras.hideStory()
|
||||
|
||||
return SignalContactRecord.Builder(rawStorageId, recipient.aci, recipient.syncExtras.storageProto)
|
||||
.setE164(recipient.e164)
|
||||
.setPni(recipient.pni)
|
||||
.setProfileKey(recipient.profileKey)
|
||||
.setProfileGivenName(recipient.signalProfileName.givenName)
|
||||
.setProfileFamilyName(recipient.signalProfileName.familyName)
|
||||
.setSystemGivenName(recipient.systemProfileName.givenName)
|
||||
.setSystemFamilyName(recipient.systemProfileName.familyName)
|
||||
.setSystemNickname(recipient.syncExtras.systemNickname)
|
||||
.setBlocked(recipient.isBlocked)
|
||||
.setProfileSharingEnabled(recipient.profileSharing || recipient.systemContactUri != null)
|
||||
.setIdentityKey(recipient.syncExtras.identityKey)
|
||||
.setIdentityState(localToRemoteIdentityState(recipient.syncExtras.identityStatus))
|
||||
.setArchived(recipient.syncExtras.isArchived)
|
||||
.setForcedUnread(recipient.syncExtras.isForcedUnread)
|
||||
.setMuteUntil(recipient.muteUntil)
|
||||
.setHideStory(hideStory)
|
||||
.setUnregisteredTimestamp(recipient.syncExtras.unregisteredTimestamp)
|
||||
.setHidden(recipient.hiddenState != Recipient.HiddenState.NOT_HIDDEN)
|
||||
.setUsername(recipient.username)
|
||||
.setPniSignatureVerified(recipient.syncExtras.pniSignatureVerified)
|
||||
.setNicknameGivenName(recipient.nickname.givenName)
|
||||
.setNicknameFamilyName(recipient.nickname.familyName)
|
||||
.setNote(recipient.note)
|
||||
.build()
|
||||
return SignalContactRecord.newBuilder(recipient.syncExtras.storageProto).apply {
|
||||
aci = recipient.aci?.toString() ?: ""
|
||||
e164 = recipient.e164 ?: ""
|
||||
pni = recipient.pni?.toStringWithoutPrefix() ?: ""
|
||||
profileKey = recipient.profileKey?.toByteString() ?: ByteString.EMPTY
|
||||
givenName = recipient.signalProfileName.givenName
|
||||
familyName = recipient.signalProfileName.familyName
|
||||
systemGivenName = recipient.systemProfileName.givenName
|
||||
systemFamilyName = recipient.systemProfileName.familyName
|
||||
systemNickname = recipient.syncExtras.systemNickname ?: ""
|
||||
blocked = recipient.isBlocked
|
||||
whitelisted = recipient.profileSharing || recipient.systemContactUri != null
|
||||
identityKey = recipient.syncExtras.identityKey?.toByteString() ?: ByteString.EMPTY
|
||||
identityState = localToRemoteIdentityState(recipient.syncExtras.identityStatus)
|
||||
archived = recipient.syncExtras.isArchived
|
||||
markedUnread = recipient.syncExtras.isForcedUnread
|
||||
mutedUntilTimestamp = recipient.muteUntil
|
||||
hideStory = recipient.extras != null && recipient.extras.hideStory()
|
||||
unregisteredAtTimestamp = recipient.syncExtras.unregisteredTimestamp
|
||||
hidden = recipient.hiddenState != Recipient.HiddenState.NOT_HIDDEN
|
||||
username = recipient.username ?: ""
|
||||
pniSignatureVerified = recipient.syncExtras.pniSignatureVerified
|
||||
nickname = recipient.nickname.takeUnless { it.isEmpty }?.let { ContactRecord.Name(given = it.givenName, family = it.familyName) }
|
||||
note = recipient.note ?: ""
|
||||
}.build().toSignalContactRecord(StorageId.forContact(rawStorageId))
|
||||
}
|
||||
|
||||
private fun localToRemoteGroupV1(recipient: RecipientRecord, rawStorageId: ByteArray): SignalGroupV1Record {
|
||||
@@ -186,13 +190,14 @@ object StorageSyncModels {
|
||||
throw AssertionError("Group is not V1")
|
||||
}
|
||||
|
||||
return SignalGroupV1Record.Builder(rawStorageId, groupId.decodedId, recipient.syncExtras.storageProto)
|
||||
.setBlocked(recipient.isBlocked)
|
||||
.setProfileSharingEnabled(recipient.profileSharing)
|
||||
.setArchived(recipient.syncExtras.isArchived)
|
||||
.setForcedUnread(recipient.syncExtras.isForcedUnread)
|
||||
.setMuteUntil(recipient.muteUntil)
|
||||
.build()
|
||||
return SignalGroupV1Record.newBuilder(recipient.syncExtras.storageProto).apply {
|
||||
id = recipient.groupId.requireV1().decodedId.toByteString()
|
||||
blocked = recipient.isBlocked
|
||||
whitelisted = recipient.profileSharing
|
||||
archived = recipient.syncExtras.isArchived
|
||||
markedUnread = recipient.syncExtras.isForcedUnread
|
||||
mutedUntilTimestamp = recipient.muteUntil
|
||||
}.build().toSignalGroupV1Record(StorageId.forGroupV1(rawStorageId))
|
||||
}
|
||||
|
||||
private fun localToRemoteGroupV2(recipient: RecipientRecord, rawStorageId: ByteArray?, groupMasterKey: GroupMasterKey): SignalGroupV2Record {
|
||||
@@ -202,29 +207,21 @@ object StorageSyncModels {
|
||||
throw AssertionError("Group is not V2")
|
||||
}
|
||||
|
||||
if (groupMasterKey == null) {
|
||||
throw AssertionError("Group master key not on recipient record")
|
||||
}
|
||||
|
||||
val hideStory = recipient.extras != null && recipient.extras.hideStory()
|
||||
val showAsStoryState = groups.getShowAsStoryState(groupId)
|
||||
|
||||
val storySendMode = when (showAsStoryState) {
|
||||
ShowAsStoryState.ALWAYS -> GroupV2Record.StorySendMode.ENABLED
|
||||
ShowAsStoryState.NEVER -> GroupV2Record.StorySendMode.DISABLED
|
||||
else -> GroupV2Record.StorySendMode.DEFAULT
|
||||
}
|
||||
|
||||
return SignalGroupV2Record.Builder(rawStorageId, groupMasterKey, recipient.syncExtras.storageProto)
|
||||
.setBlocked(recipient.isBlocked)
|
||||
.setProfileSharingEnabled(recipient.profileSharing)
|
||||
.setArchived(recipient.syncExtras.isArchived)
|
||||
.setForcedUnread(recipient.syncExtras.isForcedUnread)
|
||||
.setMuteUntil(recipient.muteUntil)
|
||||
.setNotifyForMentionsWhenMuted(recipient.mentionSetting == RecipientTable.MentionSetting.ALWAYS_NOTIFY)
|
||||
.setHideStory(hideStory)
|
||||
.setStorySendMode(storySendMode)
|
||||
.build()
|
||||
return SignalGroupV2Record.newBuilder(recipient.syncExtras.storageProto).apply {
|
||||
masterKey = groupMasterKey.serialize().toByteString()
|
||||
blocked = recipient.isBlocked
|
||||
whitelisted = recipient.profileSharing
|
||||
archived = recipient.syncExtras.isArchived
|
||||
markedUnread = recipient.syncExtras.isForcedUnread
|
||||
mutedUntilTimestamp = recipient.muteUntil
|
||||
dontNotifyForMentionsIfMuted = recipient.mentionSetting == RecipientTable.MentionSetting.ALWAYS_NOTIFY
|
||||
hideStory = recipient.extras != null && recipient.extras.hideStory()
|
||||
storySendMode = when (groups.getShowAsStoryState(groupId)) {
|
||||
ShowAsStoryState.ALWAYS -> GroupV2Record.StorySendMode.ENABLED
|
||||
ShowAsStoryState.NEVER -> GroupV2Record.StorySendMode.DISABLED
|
||||
else -> GroupV2Record.StorySendMode.DEFAULT
|
||||
}
|
||||
}.build().toSignalGroupV2Record(StorageId.forGroupV2(rawStorageId))
|
||||
}
|
||||
|
||||
private fun localToRemoteCallLink(recipient: RecipientRecord, rawStorageId: ByteArray): SignalCallLinkRecord {
|
||||
@@ -239,11 +236,11 @@ object StorageSyncModels {
|
||||
val deletedTimestamp = max(0.0, callLinks.getDeletedTimestampByRoomId(callLinkRoomId).toDouble()).toLong()
|
||||
val adminPassword = if (deletedTimestamp > 0) byteArrayOf() else callLink.credentials.adminPassBytes!!
|
||||
|
||||
return SignalCallLinkRecord.Builder(rawStorageId, null)
|
||||
.setRootKey(callLink.credentials.linkKeyBytes)
|
||||
.setAdminPassKey(adminPassword)
|
||||
.setDeletedTimestamp(deletedTimestamp)
|
||||
.build()
|
||||
return SignalCallLinkRecord.newBuilder(null).apply {
|
||||
rootKey = callLink.credentials.linkKeyBytes.toByteString()
|
||||
adminPasskey = adminPassword.toByteString()
|
||||
deletedAtTimestampMs = deletedTimestamp
|
||||
}.build().toSignalCallLinkRecord(StorageId.forCallLink(rawStorageId))
|
||||
}
|
||||
|
||||
private fun localToRemoteStoryDistributionList(recipient: RecipientRecord, rawStorageId: ByteArray): SignalStoryDistributionListRecord {
|
||||
@@ -252,25 +249,22 @@ object StorageSyncModels {
|
||||
val record = distributionLists.getListForStorageSync(distributionListId) ?: throw AssertionError("Must have a distribution list record!")
|
||||
|
||||
if (record.deletedAtTimestamp > 0L) {
|
||||
return SignalStoryDistributionListRecord.Builder(rawStorageId, recipient.syncExtras.storageProto)
|
||||
.setIdentifier(UuidUtil.toByteArray(record.distributionId.asUuid()))
|
||||
.setDeletedAtTimestamp(record.deletedAtTimestamp)
|
||||
.build()
|
||||
return SignalStoryDistributionListRecord.newBuilder(recipient.syncExtras.storageProto).apply {
|
||||
identifier = UuidUtil.toByteArray(record.distributionId.asUuid()).toByteString()
|
||||
deletedAtTimestamp = record.deletedAtTimestamp
|
||||
}.build().toSignalStoryDistributionListRecord(StorageId.forStoryDistributionList(rawStorageId))
|
||||
}
|
||||
|
||||
return SignalStoryDistributionListRecord.Builder(rawStorageId, recipient.syncExtras.storageProto)
|
||||
.setIdentifier(UuidUtil.toByteArray(record.distributionId.asUuid()))
|
||||
.setName(record.name)
|
||||
.setRecipients(
|
||||
record.getMembersToSync()
|
||||
.map { Recipient.resolved(it) }
|
||||
.filter { it.hasServiceId }
|
||||
.map { it.requireServiceId() }
|
||||
.map { SignalServiceAddress(it) }
|
||||
)
|
||||
.setAllowsReplies(record.allowsReplies)
|
||||
.setIsBlockList(record.privacyMode.isBlockList)
|
||||
.build()
|
||||
return SignalStoryDistributionListRecord.newBuilder(recipient.syncExtras.storageProto).apply {
|
||||
identifier = UuidUtil.toByteArray(record.distributionId.asUuid()).toByteString()
|
||||
name = record.name
|
||||
recipientServiceIds = record.getMembersToSync()
|
||||
.map { Recipient.resolved(it) }
|
||||
.filter { it.hasServiceId }
|
||||
.map { it.requireServiceId().toString() }
|
||||
allowsReplies = record.allowsReplies
|
||||
isBlockList = record.privacyMode.isBlockList
|
||||
}.build().toSignalStoryDistributionListRecord(StorageId.forStoryDistributionList(rawStorageId))
|
||||
}
|
||||
|
||||
fun remoteToLocalIdentityStatus(identityState: IdentityState): VerifiedStatus {
|
||||
|
||||
@@ -5,14 +5,18 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Record processor for [SignalStoryDistributionListRecord].
|
||||
* Handles merging and updating our local store when processing remote dlist storage records.
|
||||
*/
|
||||
class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<SignalStoryDistributionListRecord>() {
|
||||
|
||||
companion object {
|
||||
@@ -28,7 +32,7 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
* - A non-visually-empty name field OR a deleted at timestamp
|
||||
*/
|
||||
override fun isInvalid(remote: SignalStoryDistributionListRecord): Boolean {
|
||||
val remoteUuid = UuidUtil.parseOrNull(remote.identifier)
|
||||
val remoteUuid = UuidUtil.parseOrNull(remote.proto.identifier)
|
||||
if (remoteUuid == null) {
|
||||
Log.d(TAG, "Bad distribution list identifier -- marking as invalid")
|
||||
return true
|
||||
@@ -42,7 +46,7 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
|
||||
haveSeenMyStory = haveSeenMyStory or isMyStory
|
||||
|
||||
if (remote.deletedAtTimestamp > 0L) {
|
||||
if (remote.proto.deletedAtTimestamp > 0L) {
|
||||
if (isMyStory) {
|
||||
Log.w(TAG, "Refusing to delete My Story -- marking as invalid")
|
||||
return true
|
||||
@@ -51,7 +55,7 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtil.isVisuallyEmpty(remote.name)) {
|
||||
if (StringUtil.isVisuallyEmpty(remote.proto.name)) {
|
||||
Log.d(TAG, "Bad distribution list name (visually empty) -- marking as invalid")
|
||||
return true
|
||||
}
|
||||
@@ -62,7 +66,7 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
override fun getMatching(remote: SignalStoryDistributionListRecord, keyGenerator: StorageKeyGenerator): Optional<SignalStoryDistributionListRecord> {
|
||||
Log.d(TAG, "Attempting to get matching record...")
|
||||
val matching = SignalDatabase.distributionLists.getRecipientIdForSyncRecord(remote)
|
||||
if (matching == null && UuidUtil.parseOrThrow(remote.identifier) == DistributionId.MY_STORY.asUuid()) {
|
||||
if (matching == null && UuidUtil.parseOrThrow(remote.proto.identifier) == DistributionId.MY_STORY.asUuid()) {
|
||||
Log.e(TAG, "Cannot find matching database record for My Story.")
|
||||
throw MyStoryDoesNotExistException()
|
||||
}
|
||||
@@ -88,48 +92,24 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
}
|
||||
|
||||
override fun merge(remote: SignalStoryDistributionListRecord, local: SignalStoryDistributionListRecord, keyGenerator: StorageKeyGenerator): SignalStoryDistributionListRecord {
|
||||
val unknownFields = remote.serializeUnknownFields()
|
||||
val identifier = remote.identifier
|
||||
val name = remote.name
|
||||
val recipients = remote.recipients
|
||||
val deletedAtTimestamp = remote.deletedAtTimestamp
|
||||
val allowsReplies = remote.allowsReplies()
|
||||
val isBlockList = remote.isBlockList
|
||||
val merged = SignalStoryDistributionListRecord.newBuilder(remote.serializedUnknowns).apply {
|
||||
identifier = remote.proto.identifier
|
||||
name = remote.proto.name
|
||||
recipientServiceIds = remote.proto.recipientServiceIds
|
||||
deletedAtTimestamp = remote.proto.deletedAtTimestamp
|
||||
allowsReplies = remote.proto.allowsReplies
|
||||
isBlockList = remote.proto.isBlockList
|
||||
}.build().toSignalStoryDistributionListRecord(StorageId.forStoryDistributionList(keyGenerator.generate()))
|
||||
|
||||
val matchesRemote = doParamsMatch(
|
||||
record = remote,
|
||||
unknownFields = unknownFields,
|
||||
identifier = identifier,
|
||||
name = name,
|
||||
recipients = recipients,
|
||||
deletedAtTimestamp = deletedAtTimestamp,
|
||||
allowsReplies = allowsReplies,
|
||||
isBlockList = isBlockList
|
||||
)
|
||||
val matchesLocal = doParamsMatch(
|
||||
record = local,
|
||||
unknownFields = unknownFields,
|
||||
identifier = identifier,
|
||||
name = name,
|
||||
recipients = recipients,
|
||||
deletedAtTimestamp = deletedAtTimestamp,
|
||||
allowsReplies = allowsReplies,
|
||||
isBlockList = isBlockList
|
||||
)
|
||||
val matchesRemote = doParamsMatch(remote, merged)
|
||||
val matchesLocal = doParamsMatch(local, merged)
|
||||
|
||||
return if (matchesRemote) {
|
||||
remote
|
||||
} else if (matchesLocal) {
|
||||
local
|
||||
} else {
|
||||
SignalStoryDistributionListRecord.Builder(keyGenerator.generate(), unknownFields)
|
||||
.setIdentifier(identifier)
|
||||
.setName(name)
|
||||
.setRecipients(recipients)
|
||||
.setDeletedAtTimestamp(deletedAtTimestamp)
|
||||
.setAllowsReplies(allowsReplies)
|
||||
.setIsBlockList(isBlockList)
|
||||
.build()
|
||||
merged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,44 +123,19 @@ class StoryDistributionListRecordProcessor : DefaultStorageRecordProcessor<Signa
|
||||
}
|
||||
|
||||
override fun compare(o1: SignalStoryDistributionListRecord, o2: SignalStoryDistributionListRecord): Int {
|
||||
return if (o1.identifier.contentEquals(o2.identifier)) {
|
||||
return if (o1.proto.identifier == o2.proto.identifier) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
private fun doParamsMatch(
|
||||
record: SignalStoryDistributionListRecord,
|
||||
unknownFields: ByteArray?,
|
||||
identifier: ByteArray?,
|
||||
name: String?,
|
||||
recipients: List<SignalServiceAddress>,
|
||||
deletedAtTimestamp: Long,
|
||||
allowsReplies: Boolean,
|
||||
isBlockList: Boolean
|
||||
): Boolean {
|
||||
return unknownFields.contentEquals(record.serializeUnknownFields()) &&
|
||||
identifier.contentEquals(record.identifier) &&
|
||||
name == record.name &&
|
||||
recipients == record.recipients &&
|
||||
deletedAtTimestamp == record.deletedAtTimestamp &&
|
||||
allowsReplies == record.allowsReplies() &&
|
||||
isBlockList == record.isBlockList
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the RecipientSettings object for a given distribution list is not the
|
||||
* correct group type (4).
|
||||
*/
|
||||
private class InvalidGroupTypeException : RuntimeException()
|
||||
|
||||
/**
|
||||
* Thrown when the distribution list object returned from the storage sync helper is
|
||||
* absent, even though a RecipientSettings was found.
|
||||
*/
|
||||
private class UnexpectedEmptyOptionalException : RuntimeException()
|
||||
|
||||
/**
|
||||
* Thrown when we try to ge the matching record for the "My Story" distribution ID but
|
||||
* it isn't in the database.
|
||||
|
||||
@@ -307,9 +307,9 @@ class ContactRecordProcessorTest {
|
||||
val result = subject.merge(remote, local, TestKeyGenerator(STORAGE_ID_C))
|
||||
|
||||
// THEN
|
||||
assertEquals(local.aci, result.aci)
|
||||
assertEquals(local.number.get(), result.number.get())
|
||||
assertEquals(local.pni.get(), result.pni.get())
|
||||
assertEquals(local.proto.aci, result.proto.aci)
|
||||
assertEquals(local.proto.e164, result.proto.e164)
|
||||
assertEquals(local.proto.pni, result.proto.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -339,9 +339,9 @@ class ContactRecordProcessorTest {
|
||||
val result = subject.merge(remote, local, TestKeyGenerator(STORAGE_ID_C))
|
||||
|
||||
// THEN
|
||||
assertEquals(local.aci, result.aci)
|
||||
assertEquals(local.number.get(), result.number.get())
|
||||
assertEquals(local.pni.get(), result.pni.get())
|
||||
assertEquals(local.proto.aci, result.proto.aci)
|
||||
assertEquals(local.proto.e164, result.proto.e164)
|
||||
assertEquals(local.proto.pni, result.proto.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -371,9 +371,9 @@ class ContactRecordProcessorTest {
|
||||
val result = subject.merge(remote, local, TestKeyGenerator(STORAGE_ID_C))
|
||||
|
||||
// THEN
|
||||
assertEquals(remote.aci, result.aci)
|
||||
assertEquals(remote.number.get(), result.number.get())
|
||||
assertEquals(remote.pni.get(), result.pni.get())
|
||||
assertEquals(remote.proto.aci, result.proto.aci)
|
||||
assertEquals(remote.proto.e164, result.proto.e164)
|
||||
assertEquals(remote.proto.pni, result.proto.pni)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -403,9 +403,9 @@ class ContactRecordProcessorTest {
|
||||
val result = subject.merge(remote, local, TestKeyGenerator(STORAGE_ID_C))
|
||||
|
||||
// THEN
|
||||
assertEquals("Ghost", result.nicknameGivenName.get())
|
||||
assertEquals("Spider", result.nicknameFamilyName.get())
|
||||
assertEquals("Spidey Friend", result.note.get())
|
||||
assertEquals("Ghost", result.proto.nickname?.given)
|
||||
assertEquals("Spider", result.proto.nickname?.family)
|
||||
assertEquals("Spidey Friend", result.proto.note)
|
||||
}
|
||||
|
||||
private fun buildRecord(id: StorageId = STORAGE_ID_A, record: ContactRecord): SignalContactRecord {
|
||||
|
||||
@@ -14,13 +14,10 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -28,6 +25,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -132,13 +131,16 @@ public final class StorageSyncHelperTest {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setProfileKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
|
||||
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKey)).build();
|
||||
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKeyCopy)).build();
|
||||
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
|
||||
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
|
||||
|
||||
assertFalse(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
||||
assertEquals(signalContactA, signalContactB);
|
||||
assertEquals(signalContactA.hashCode(), signalContactB.hashCode());
|
||||
|
||||
assertFalse(StorageSyncHelper.profileKeyChanged(update(signalContactA, signalContactB)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,23 +149,23 @@ public final class StorageSyncHelperTest {
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setProfileKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
|
||||
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKey)).build();
|
||||
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").profileKey(ByteString.of(profileKeyCopy)).build();
|
||||
|
||||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
|
||||
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
|
||||
|
||||
assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
||||
assertNotEquals(signalContactA, signalContactB);
|
||||
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode());
|
||||
|
||||
assertTrue(StorageSyncHelper.profileKeyChanged(update(signalContactA, signalContactB)));
|
||||
}
|
||||
|
||||
private static SignalContactRecord.Builder contactBuilder(int key,
|
||||
ACI aci,
|
||||
String e164,
|
||||
String profileName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), aci, null)
|
||||
.setE164(e164)
|
||||
.setProfileGivenName(profileName);
|
||||
private static ContactRecord.Builder contactBuilder(ACI aci, String e164, String profileName) {
|
||||
return new ContactRecord.Builder()
|
||||
.aci(aci.toString())
|
||||
.e164(e164)
|
||||
.givenName(profileName);
|
||||
}
|
||||
|
||||
private static <E extends SignalRecord<?>> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
|
||||
|
||||
Reference in New Issue
Block a user