The rest of the storage service unwrapping.
This commit is contained in:
parent
8746f483c0
commit
7dd1fc09c0
33 changed files with 754 additions and 1692 deletions
|
@ -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) {
|
||||
|
|
|
@ -37,7 +37,7 @@ sealed class ServiceId(val libSignalServiceId: LibSignalServiceId) {
|
|||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun parseOrNull(raw: String?, logFailures: Boolean = true): ServiceId? {
|
||||
if (raw == null) {
|
||||
if (raw.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
|
||||
val ContactRecord.signalAci: ServiceId.ACI?
|
||||
get() = ServiceId.ACI.parseOrNull(this.aci)
|
||||
|
||||
val ContactRecord.signalPni: ServiceId.PNI?
|
||||
get() = ServiceId.PNI.parseOrNull(this.pni)
|
|
@ -1,55 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.signal.core.util.hasUnknownFields
|
||||
import org.signal.libsignal.protocol.logging.Log
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
import java.io.IOException
|
||||
|
||||
class SignalAccountRecord(
|
||||
/**
|
||||
* Wrapper around a [AccountRecord] to pair it with a [StorageId].
|
||||
*/
|
||||
data class SignalAccountRecord(
|
||||
override val id: StorageId,
|
||||
override val proto: AccountRecord
|
||||
) : SignalRecord<AccountRecord> {
|
||||
|
||||
companion object {
|
||||
private val TAG: String = SignalAccountRecord::class.java.simpleName
|
||||
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): AccountRecord.Builder {
|
||||
return if (serializedUnknowns != null) {
|
||||
parseUnknowns(serializedUnknowns)
|
||||
} else {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: AccountRecord.Builder()
|
||||
}
|
||||
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder {
|
||||
return try {
|
||||
AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
AccountRecord.Builder()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder {
|
||||
try {
|
||||
return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e)
|
||||
return AccountRecord.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun serializeUnknownFields(): ByteArray? {
|
||||
return if (proto.hasUnknownFields()) proto.encode() else null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SignalAccountRecord
|
||||
|
||||
if (id != other.id) return false
|
||||
if (proto != other.proto) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + proto.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,64 +5,27 @@
|
|||
|
||||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A record in storage service that represents a call link that was already created.
|
||||
* Wrapper around a [CallLinkRecord] to pair it with a [StorageId].
|
||||
*/
|
||||
class SignalCallLinkRecord(
|
||||
data class SignalCallLinkRecord(
|
||||
override val id: StorageId,
|
||||
override val proto: CallLinkRecord
|
||||
) : SignalRecord<CallLinkRecord> {
|
||||
|
||||
val rootKey: ByteArray = proto.rootKey.toByteArray()
|
||||
val adminPassKey: ByteArray = proto.adminPasskey.toByteArray()
|
||||
val deletionTimestamp: Long = proto.deletedAtTimestampMs
|
||||
|
||||
fun isDeleted(): Boolean {
|
||||
return deletionTimestamp > 0
|
||||
}
|
||||
|
||||
class Builder(rawId: ByteArray, serializedUnknowns: ByteArray?) {
|
||||
private var id: StorageId = StorageId.forCallLink(rawId)
|
||||
private var builder: CallLinkRecord.Builder
|
||||
|
||||
init {
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns)
|
||||
} else {
|
||||
this.builder = CallLinkRecord.Builder()
|
||||
}
|
||||
companion object {
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): CallLinkRecord.Builder {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: CallLinkRecord.Builder()
|
||||
}
|
||||
|
||||
fun setRootKey(rootKey: ByteArray): Builder {
|
||||
builder.rootKey = rootKey.toByteString()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setAdminPassKey(adminPasskey: ByteArray): Builder {
|
||||
builder.adminPasskey = adminPasskey.toByteString()
|
||||
return this
|
||||
}
|
||||
|
||||
fun setDeletedTimestamp(deletedTimestamp: Long): Builder {
|
||||
builder.deletedAtTimestampMs = deletedTimestamp
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): SignalCallLinkRecord {
|
||||
return SignalCallLinkRecord(id, builder.build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parseUnknowns(serializedUnknowns: ByteArray): CallLinkRecord.Builder {
|
||||
return try {
|
||||
CallLinkRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
CallLinkRecord.Builder()
|
||||
}
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): CallLinkRecord.Builder {
|
||||
return try {
|
||||
CallLinkRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
CallLinkRecord.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,360 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.ProtoUtil;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public final class SignalContactRecord implements SignalRecord<ContactRecord> {
|
||||
|
||||
private static final String TAG = SignalContactRecord.class.getSimpleName();
|
||||
|
||||
private final StorageId id;
|
||||
private final ContactRecord proto;
|
||||
private final boolean hasUnknownFields;
|
||||
|
||||
private final Optional<ACI> aci;
|
||||
private final Optional<PNI> pni;
|
||||
private final Optional<String> e164;
|
||||
private final Optional<String> profileGivenName;
|
||||
private final Optional<String> profileFamilyName;
|
||||
private final Optional<String> systemGivenName;
|
||||
private final Optional<String> systemFamilyName;
|
||||
private final Optional<String> systemNickname;
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<String> username;
|
||||
private final Optional<byte[]> identityKey;
|
||||
private final Optional<String> nicknameGivenName;
|
||||
private final Optional<String> nicknameFamilyName;
|
||||
private final Optional<String> note;
|
||||
|
||||
public SignalContactRecord(StorageId id, ContactRecord proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||
this.aci = OptionalUtil.absentIfEmpty(proto.aci).map(ACI::parseOrNull).map(it -> it.isUnknown() ? null : it);
|
||||
this.pni = OptionalUtil.absentIfEmpty(proto.pni).map(PNI::parseOrNull).map(it -> it.isUnknown() ? null : it);
|
||||
this.e164 = OptionalUtil.absentIfEmpty(proto.e164);
|
||||
this.profileGivenName = OptionalUtil.absentIfEmpty(proto.givenName);
|
||||
this.profileFamilyName = OptionalUtil.absentIfEmpty(proto.familyName);
|
||||
this.systemGivenName = OptionalUtil.absentIfEmpty(proto.systemGivenName);
|
||||
this.systemFamilyName = OptionalUtil.absentIfEmpty(proto.systemFamilyName);
|
||||
this.systemNickname = OptionalUtil.absentIfEmpty(proto.systemNickname);
|
||||
this.profileKey = OptionalUtil.absentIfEmpty(proto.profileKey);
|
||||
this.username = OptionalUtil.absentIfEmpty(proto.username);
|
||||
this.identityKey = OptionalUtil.absentIfEmpty(proto.identityKey);
|
||||
this.nicknameGivenName = Optional.ofNullable(proto.nickname).flatMap(n -> OptionalUtil.absentIfEmpty(n.given));
|
||||
this.nicknameFamilyName = Optional.ofNullable(proto.nickname).flatMap(n -> OptionalUtil.absentIfEmpty(n.family));
|
||||
this.note = OptionalUtil.absentIfEmpty(proto.note);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactRecord getProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public boolean hasUnknownFields() {
|
||||
return hasUnknownFields;
|
||||
}
|
||||
|
||||
public byte[] serializeUnknownFields() {
|
||||
return hasUnknownFields ? proto.encode() : null;
|
||||
}
|
||||
|
||||
public Optional<ACI> getAci() {
|
||||
return aci;
|
||||
}
|
||||
|
||||
public Optional<PNI> getPni() {
|
||||
return pni;
|
||||
}
|
||||
|
||||
public Optional<? extends ServiceId> getServiceId() {
|
||||
if (aci.isPresent()) {
|
||||
return aci;
|
||||
} else if (pni.isPresent()) {
|
||||
return pni;
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<String> getNumber() {
|
||||
return e164;
|
||||
}
|
||||
|
||||
public Optional<String> getProfileGivenName() {
|
||||
return profileGivenName;
|
||||
}
|
||||
|
||||
public Optional<String> getProfileFamilyName() {
|
||||
return profileFamilyName;
|
||||
}
|
||||
|
||||
public Optional<String> getSystemGivenName() {
|
||||
return systemGivenName;
|
||||
}
|
||||
|
||||
public Optional<String> getSystemFamilyName() {
|
||||
return systemFamilyName;
|
||||
}
|
||||
|
||||
public Optional<String> getSystemNickname() {
|
||||
return systemNickname;
|
||||
}
|
||||
|
||||
public Optional<String> getNicknameGivenName() {
|
||||
return nicknameGivenName;
|
||||
}
|
||||
|
||||
public Optional<String> getNicknameFamilyName() {
|
||||
return nicknameFamilyName;
|
||||
}
|
||||
|
||||
public Optional<String> getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public Optional<String> getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public IdentityState getIdentityState() {
|
||||
return proto.identityState;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return proto.blocked;
|
||||
}
|
||||
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return proto.whitelisted;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return proto.archived;
|
||||
}
|
||||
|
||||
public boolean isForcedUnread() {
|
||||
return proto.markedUnread;
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.mutedUntilTimestamp;
|
||||
}
|
||||
|
||||
public boolean shouldHideStory() {
|
||||
return proto.hideStory;
|
||||
}
|
||||
|
||||
public long getUnregisteredTimestamp() {
|
||||
return proto.unregisteredAtTimestamp;
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return proto.hidden;
|
||||
}
|
||||
|
||||
public boolean isPniSignatureVerified() {
|
||||
return proto.pniSignatureVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the same record, but stripped of the PNI field. Only used while PNP is in development.
|
||||
*/
|
||||
public SignalContactRecord withoutPni() {
|
||||
return new SignalContactRecord(id, proto.newBuilder().pni("").build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalContactRecord that = (SignalContactRecord) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final ContactRecord.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, @Nullable ACI aci, byte[] serializedUnknowns) {
|
||||
this.id = StorageId.forContact(rawId);
|
||||
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns);
|
||||
} else {
|
||||
this.builder = new ContactRecord.Builder();
|
||||
}
|
||||
|
||||
builder.aci(aci == null ? "" : aci.toString());
|
||||
}
|
||||
|
||||
public Builder setE164(String e164) {
|
||||
builder.e164(e164 == null ? "" : e164);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPni(PNI pni) {
|
||||
builder.pni(pni == null ? "" : pni.toStringWithoutPrefix());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileGivenName(String givenName) {
|
||||
builder.givenName(givenName == null ? "" : givenName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileFamilyName(String familyName) {
|
||||
builder.familyName(familyName == null ? "" : familyName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSystemGivenName(String givenName) {
|
||||
builder.systemGivenName(givenName == null ? "" : givenName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSystemFamilyName(String familyName) {
|
||||
builder.systemFamilyName(familyName == null ? "" : familyName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSystemNickname(String nickname) {
|
||||
builder.systemNickname(nickname == null ? "" : nickname);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileKey(byte[] profileKey) {
|
||||
builder.profileKey(profileKey == null ? ByteString.EMPTY : ByteString.of(profileKey));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUsername(String username) {
|
||||
builder.username(username == null ? "" : username);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIdentityKey(byte[] identityKey) {
|
||||
builder.identityKey(identityKey == null ? ByteString.EMPTY : ByteString.of(identityKey));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIdentityState(IdentityState identityState) {
|
||||
builder.identityState(identityState == null ? IdentityState.DEFAULT : identityState);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBlocked(boolean blocked) {
|
||||
builder.blocked(blocked);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
|
||||
builder.whitelisted(profileSharingEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setArchived(boolean archived) {
|
||||
builder.archived(archived);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setForcedUnread(boolean forcedUnread) {
|
||||
builder.markedUnread(forcedUnread);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.mutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHideStory(boolean hideStory) {
|
||||
builder.hideStory(hideStory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnregisteredTimestamp(long timestamp) {
|
||||
builder.unregisteredAtTimestamp(timestamp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHidden(boolean hidden) {
|
||||
builder.hidden(hidden);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPniSignatureVerified(boolean verified) {
|
||||
builder.pniSignatureVerified(verified);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNicknameGivenName(String nicknameGivenName) {
|
||||
ContactRecord.Name.Builder name = builder.nickname == null ? new ContactRecord.Name.Builder() : builder.nickname.newBuilder();
|
||||
name.given(nicknameGivenName);
|
||||
builder.nickname(name.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNicknameFamilyName(String nicknameFamilyName) {
|
||||
ContactRecord.Name.Builder name = builder.nickname == null ? new ContactRecord.Name.Builder() : builder.nickname.newBuilder();
|
||||
name.family(nicknameFamilyName);
|
||||
builder.nickname(name.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNote(String note) {
|
||||
builder.note(note == null ? "" : note);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static ContactRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
|
||||
try {
|
||||
return ContactRecord.ADAPTER.decode(serializedUnknowns).newBuilder();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e);
|
||||
return new ContactRecord.Builder();
|
||||
}
|
||||
}
|
||||
|
||||
public SignalContactRecord build() {
|
||||
return new SignalContactRecord(id, builder.build());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Wrapper around a [ContactRecord] to pair it with a [StorageId].
|
||||
*/
|
||||
data class SignalContactRecord(
|
||||
override val id: StorageId,
|
||||
override val proto: ContactRecord
|
||||
) : SignalRecord<ContactRecord> {
|
||||
|
||||
companion object {
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): ContactRecord.Builder {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: ContactRecord.Builder()
|
||||
}
|
||||
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): ContactRecord.Builder {
|
||||
return try {
|
||||
ContactRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
ContactRecord.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.signal.core.util.ProtoUtil;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public final class SignalGroupV1Record implements SignalRecord<GroupV1Record> {
|
||||
|
||||
private static final String TAG = SignalGroupV1Record.class.getSimpleName();
|
||||
|
||||
private final StorageId id;
|
||||
private final GroupV1Record proto;
|
||||
private final byte[] groupId;
|
||||
private final boolean hasUnknownFields;
|
||||
|
||||
public SignalGroupV1Record(StorageId id, GroupV1Record proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.groupId = proto.id.toByteArray();
|
||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override public GroupV1Record getProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public boolean hasUnknownFields() {
|
||||
return hasUnknownFields;
|
||||
}
|
||||
|
||||
public byte[] serializeUnknownFields() {
|
||||
return hasUnknownFields ? proto.encode() : null;
|
||||
}
|
||||
|
||||
public byte[] getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return proto.blocked;
|
||||
}
|
||||
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return proto.whitelisted;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return proto.archived;
|
||||
}
|
||||
|
||||
public boolean isForcedUnread() {
|
||||
return proto.markedUnread;
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.mutedUntilTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalGroupV1Record that = (SignalGroupV1Record) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final GroupV1Record.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, byte[] groupId, byte[] serializedUnknowns) {
|
||||
this.id = StorageId.forGroupV1(rawId);
|
||||
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns);
|
||||
} else {
|
||||
this.builder = new GroupV1Record.Builder();
|
||||
}
|
||||
|
||||
builder.id(ByteString.of(groupId));
|
||||
}
|
||||
|
||||
public Builder setBlocked(boolean blocked) {
|
||||
builder.blocked(blocked);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
|
||||
builder.whitelisted(profileSharingEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setArchived(boolean archived) {
|
||||
builder.archived(archived);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setForcedUnread(boolean forcedUnread) {
|
||||
builder.markedUnread(forcedUnread);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.mutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static GroupV1Record.Builder parseUnknowns(byte[] serializedUnknowns) {
|
||||
try {
|
||||
return GroupV1Record.ADAPTER.decode(serializedUnknowns).newBuilder();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e);
|
||||
return new GroupV1Record.Builder();
|
||||
}
|
||||
}
|
||||
|
||||
public SignalGroupV1Record build() {
|
||||
return new SignalGroupV1Record(id, builder.build());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Wrapper around a [GroupV1Record] to pair it with a [StorageId].
|
||||
*/
|
||||
data class SignalGroupV1Record(
|
||||
override val id: StorageId,
|
||||
override val proto: GroupV1Record
|
||||
) : SignalRecord<GroupV1Record> {
|
||||
|
||||
companion object {
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): GroupV1Record.Builder {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: GroupV1Record.Builder()
|
||||
}
|
||||
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): GroupV1Record.Builder {
|
||||
return try {
|
||||
GroupV1Record.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
GroupV1Record.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.ProtoUtil;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public final class SignalGroupV2Record implements SignalRecord<GroupV2Record> {
|
||||
|
||||
private static final String TAG = SignalGroupV2Record.class.getSimpleName();
|
||||
|
||||
private final StorageId id;
|
||||
private final GroupV2Record proto;
|
||||
private final byte[] masterKey;
|
||||
private final boolean hasUnknownFields;
|
||||
|
||||
public SignalGroupV2Record(StorageId id, GroupV2Record proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||
this.masterKey = proto.masterKey.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override public GroupV2Record getProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public boolean hasUnknownFields() {
|
||||
return hasUnknownFields;
|
||||
}
|
||||
|
||||
public byte[] serializeUnknownFields() {
|
||||
return hasUnknownFields ? proto.encode() : null;
|
||||
}
|
||||
|
||||
public byte[] getMasterKeyBytes() {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
public GroupMasterKey getMasterKeyOrThrow() {
|
||||
try {
|
||||
return new GroupMasterKey(masterKey);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return proto.blocked;
|
||||
}
|
||||
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return proto.whitelisted;
|
||||
}
|
||||
|
||||
public boolean isArchived() {
|
||||
return proto.archived;
|
||||
}
|
||||
|
||||
public boolean isForcedUnread() {
|
||||
return proto.markedUnread;
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.mutedUntilTimestamp;
|
||||
}
|
||||
|
||||
public boolean notifyForMentionsWhenMuted() {
|
||||
return !proto.dontNotifyForMentionsIfMuted;
|
||||
}
|
||||
|
||||
public boolean shouldHideStory() {
|
||||
return proto.hideStory;
|
||||
}
|
||||
|
||||
public GroupV2Record.StorySendMode getStorySendMode() {
|
||||
return proto.storySendMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalGroupV2Record that = (SignalGroupV2Record) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final GroupV2Record.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, GroupMasterKey masterKey, byte[] serializedUnknowns) {
|
||||
this(rawId, masterKey.serialize(), serializedUnknowns);
|
||||
}
|
||||
|
||||
public Builder(byte[] rawId, byte[] masterKey, byte[] serializedUnknowns) {
|
||||
this.id = StorageId.forGroupV2(rawId);
|
||||
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns);
|
||||
} else {
|
||||
this.builder = new GroupV2Record.Builder();
|
||||
}
|
||||
|
||||
builder.masterKey(ByteString.of(masterKey));
|
||||
}
|
||||
|
||||
public Builder setBlocked(boolean blocked) {
|
||||
builder.blocked(blocked);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
|
||||
builder.whitelisted(profileSharingEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setArchived(boolean archived) {
|
||||
builder.archived(archived);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setForcedUnread(boolean forcedUnread) {
|
||||
builder.markedUnread(forcedUnread);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.mutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNotifyForMentionsWhenMuted(boolean value) {
|
||||
builder.dontNotifyForMentionsIfMuted(!value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHideStory(boolean hideStory) {
|
||||
builder.hideStory(hideStory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setStorySendMode(GroupV2Record.StorySendMode storySendMode) {
|
||||
builder.storySendMode(storySendMode);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static GroupV2Record.Builder parseUnknowns(byte[] serializedUnknowns) {
|
||||
try {
|
||||
return GroupV2Record.ADAPTER.decode(serializedUnknowns).newBuilder();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e);
|
||||
return new GroupV2Record.Builder();
|
||||
}
|
||||
}
|
||||
|
||||
public SignalGroupV2Record build() {
|
||||
return new SignalGroupV2Record(id, builder.build());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Wrapper around a [GroupV2Record] to pair it with a [StorageId].
|
||||
*/
|
||||
data class SignalGroupV2Record(
|
||||
override val id: StorageId,
|
||||
override val proto: GroupV2Record
|
||||
) : SignalRecord<GroupV2Record> {
|
||||
|
||||
companion object {
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): GroupV2Record.Builder {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: GroupV2Record.Builder()
|
||||
}
|
||||
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): GroupV2Record.Builder {
|
||||
return try {
|
||||
GroupV2Record.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
GroupV2Record.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,20 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import com.squareup.wire.Message
|
||||
import org.signal.core.util.hasUnknownFields
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
/**
|
||||
* Pairs a storage record with its id. Also contains some useful common methods.
|
||||
*/
|
||||
interface SignalRecord<E> {
|
||||
val id: StorageId
|
||||
val proto: E
|
||||
|
||||
val serializedUnknowns: ByteArray?
|
||||
get() = (proto as Message<*, *>).takeIf { it.hasUnknownFields() }?.encode()
|
||||
|
||||
fun describeDiff(other: SignalRecord<*>): String {
|
||||
if (this::class != other::class) {
|
||||
return "Classes are different!"
|
||||
|
|
|
@ -64,33 +64,12 @@ object SignalStorageModels {
|
|||
|
||||
@JvmStatic
|
||||
fun localToRemoteStorageRecord(record: SignalStorageRecord, storageKey: StorageKey): StorageItem {
|
||||
val builder = StorageRecord.Builder()
|
||||
|
||||
if (record.proto.contact != null) {
|
||||
builder.contact(record.proto.contact)
|
||||
} else if (record.proto.groupV1 != null) {
|
||||
builder.groupV1(record.proto.groupV1)
|
||||
} else if (record.proto.groupV2 != null) {
|
||||
builder.groupV2(record.proto.groupV2)
|
||||
} else if (record.proto.account != null) {
|
||||
builder.account(record.proto.account)
|
||||
} else if (record.proto.storyDistributionList != null) {
|
||||
builder.storyDistributionList(record.proto.storyDistributionList)
|
||||
} else if (record.proto.callLink != null) {
|
||||
builder.callLink(record.proto.callLink)
|
||||
} else {
|
||||
throw InvalidStorageWriteError()
|
||||
}
|
||||
|
||||
val remoteRecord = builder.build()
|
||||
val itemKey = storageKey.deriveItemKey(record.id.raw)
|
||||
val encryptedRecord = SignalStorageCipher.encrypt(itemKey, remoteRecord.encode())
|
||||
val encryptedRecord = SignalStorageCipher.encrypt(itemKey, record.proto.encode())
|
||||
|
||||
return StorageItem.Builder()
|
||||
.key(record.id.raw.toByteString())
|
||||
.value_(encryptedRecord.toByteString())
|
||||
.build()
|
||||
}
|
||||
|
||||
private class InvalidStorageWriteError : Error()
|
||||
}
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.ProtoUtil;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
public class SignalStoryDistributionListRecord implements SignalRecord<StoryDistributionListRecord> {
|
||||
|
||||
private static final String TAG = SignalStoryDistributionListRecord.class.getSimpleName();
|
||||
|
||||
private final StorageId id;
|
||||
private final StoryDistributionListRecord proto;
|
||||
private final boolean hasUnknownFields;
|
||||
private final List<SignalServiceAddress> recipients;
|
||||
|
||||
public SignalStoryDistributionListRecord(StorageId id, StoryDistributionListRecord proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
|
||||
this.recipients = proto.recipientServiceIds
|
||||
.stream()
|
||||
.map(ServiceId::parseOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.map(SignalServiceAddress::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoryDistributionListRecord getProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
public byte[] serializeUnknownFields() {
|
||||
return hasUnknownFields ? proto.encode() : null;
|
||||
}
|
||||
|
||||
public byte[] getIdentifier() {
|
||||
return proto.identifier.toByteArray();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return proto.name;
|
||||
}
|
||||
|
||||
public List<SignalServiceAddress> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public long getDeletedAtTimestamp() {
|
||||
return proto.deletedAtTimestamp;
|
||||
}
|
||||
|
||||
public boolean allowsReplies() {
|
||||
return proto.allowsReplies;
|
||||
}
|
||||
|
||||
public boolean isBlockList() {
|
||||
return proto.isBlockList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalStoryDistributionListRecord that = (SignalStoryDistributionListRecord) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final StoryDistributionListRecord.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, byte[] serializedUnknowns) {
|
||||
this.id = StorageId.forStoryDistributionList(rawId);
|
||||
|
||||
if (serializedUnknowns != null) {
|
||||
this.builder = parseUnknowns(serializedUnknowns);
|
||||
} else {
|
||||
this.builder = new StoryDistributionListRecord.Builder();
|
||||
}
|
||||
}
|
||||
|
||||
public Builder setIdentifier(byte[] identifier) {
|
||||
builder.identifier(ByteString.of(identifier));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setName(String name) {
|
||||
builder.name(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRecipients(List<SignalServiceAddress> recipients) {
|
||||
builder.recipientServiceIds = recipients.stream()
|
||||
.map(SignalServiceAddress::getIdentifier)
|
||||
.collect(Collectors.toList());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDeletedAtTimestamp(long deletedAtTimestamp) {
|
||||
builder.deletedAtTimestamp(deletedAtTimestamp);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowsReplies(boolean allowsReplies) {
|
||||
builder.allowsReplies(allowsReplies);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIsBlockList(boolean isBlockList) {
|
||||
builder.isBlockList(isBlockList);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalStoryDistributionListRecord build() {
|
||||
return new SignalStoryDistributionListRecord(id, builder.build());
|
||||
}
|
||||
|
||||
private static StoryDistributionListRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
|
||||
try {
|
||||
return StoryDistributionListRecord.ADAPTER.decode(serializedUnknowns).newBuilder();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to combine unknown fields!", e);
|
||||
return new StoryDistributionListRecord.Builder();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
||||
import java.io.IOException
|
||||
|
||||
data class SignalStoryDistributionListRecord(
|
||||
override val id: StorageId,
|
||||
override val proto: StoryDistributionListRecord
|
||||
) : SignalRecord<StoryDistributionListRecord> {
|
||||
|
||||
companion object {
|
||||
fun newBuilder(serializedUnknowns: ByteArray?): StoryDistributionListRecord.Builder {
|
||||
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: StoryDistributionListRecord.Builder()
|
||||
}
|
||||
|
||||
private fun builderFromUnknowns(serializedUnknowns: ByteArray): StoryDistributionListRecord.Builder {
|
||||
return try {
|
||||
StoryDistributionListRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||
} catch (e: IOException) {
|
||||
StoryDistributionListRecord.Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
|||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A copy of {@link ManifestRecord.Identifier} that allows us to more easily store unknown types with their integer constant.
|
||||
*/
|
||||
public class StorageId {
|
||||
private final int type;
|
||||
private final byte[] raw;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
||||
|
||||
val StoryDistributionListRecord.recipientServiceAddresses: List<SignalServiceAddress>
|
||||
get() {
|
||||
return this.recipientServiceIds
|
||||
.mapNotNull { ServiceId.parseOrNull(it) }
|
||||
.map { SignalServiceAddress(it) }
|
||||
}
|
|
@ -46,6 +46,10 @@ public final class UuidUtil {
|
|||
return new UUID(high, low);
|
||||
}
|
||||
|
||||
public static UUID parseOrThrow(ByteString bytes) {
|
||||
return parseOrNull(bytes.toByteArray());
|
||||
}
|
||||
|
||||
public static boolean isUuid(String uuid) {
|
||||
return uuid != null && UUID_PATTERN.matcher(uuid).matches();
|
||||
}
|
||||
|
@ -83,6 +87,10 @@ public final class UuidUtil {
|
|||
return byteArray != null && byteArray.length == 16 ? parseOrThrow(byteArray) : null;
|
||||
}
|
||||
|
||||
public static UUID parseOrNull(ByteString byteString) {
|
||||
return parseOrNull(byteString.toByteArray());
|
||||
}
|
||||
|
||||
public static List<UUID> fromByteStrings(Collection<ByteString> byteStringCollection) {
|
||||
ArrayList<UUID> result = new ArrayList<>(byteStringCollection.size());
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
@ -14,27 +16,33 @@ public class SignalContactRecordTest {
|
|||
|
||||
@Test
|
||||
public void contacts_with_same_identity_key_contents_are_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
byte[] identityKey = new byte[32];
|
||||
byte[] identityKeyCopy = identityKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
|
||||
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).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);
|
||||
|
||||
assertEquals(signalContactA, signalContactB);
|
||||
assertEquals(signalContactA.hashCode(), signalContactB.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_identity_key_contents_are_not_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
byte[] identityKey = new byte[32];
|
||||
byte[] identityKeyCopy = identityKey.clone();
|
||||
identityKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
|
||||
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).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);
|
||||
|
||||
assertNotEquals(signalContactA, signalContactB);
|
||||
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode());
|
||||
}
|
||||
|
||||
private static byte[] byteArray(int a) {
|
||||
|
@ -46,13 +54,9 @@ public class SignalContactRecordTest {
|
|||
return bytes;
|
||||
}
|
||||
|
||||
private static SignalContactRecord.Builder contactBuilder(int key,
|
||||
ACI serviceId,
|
||||
String e164,
|
||||
String givenName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), serviceId, null)
|
||||
.setE164(e164)
|
||||
.setProfileGivenName(givenName);
|
||||
private static ContactRecord.Builder contactBuilder(ACI serviceId, String e164, String givenName) {
|
||||
return new ContactRecord.Builder()
|
||||
.e164(e164)
|
||||
.givenName(givenName);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue