Add notification profile id for backupsv2.

This commit is contained in:
Cody Henthorne
2025-04-21 16:43:19 -04:00
parent b4a9189068
commit 4304ae2a96
21 changed files with 114 additions and 10 deletions

View File

@@ -187,6 +187,10 @@ object ImportSkips {
return log(0, "Failed to parse chatFolderId for the provided chat folder.") return log(0, "Failed to parse chatFolderId for the provided chat folder.")
} }
fun notificationProfileIdNotFound(): String {
return log(0, "Failed to parse notificationProfileId for the provided notification profile.")
}
private fun log(sentTimestamp: Long, message: String): String { private fun log(sentTimestamp: Long, message: String): String {
return "[SKIP][$sentTimestamp] $message" return "[SKIP][$sentTimestamp] $message"
} }

View File

@@ -5,10 +5,12 @@
package org.thoughtcrime.securesms.backup.v2.processor package org.thoughtcrime.securesms.backup.v2.processor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.insertInto import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.toInt import org.signal.core.util.toInt
import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportSkips
import org.thoughtcrime.securesms.backup.v2.ImportState import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -20,7 +22,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.serialize import org.thoughtcrime.securesms.database.serialize
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import java.lang.IllegalStateException import org.whispersystems.signalservice.api.util.UuidUtil
import java.time.DayOfWeek import java.time.DayOfWeek
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
@@ -41,6 +43,12 @@ object NotificationProfileProcessor {
} }
fun import(profile: NotificationProfileProto, importState: ImportState) { fun import(profile: NotificationProfileProto, importState: ImportState) {
val notificationProfileUuid = UuidUtil.parseOrNull(profile.id)
if (notificationProfileUuid == null) {
ImportSkips.notificationProfileIdNotFound()
return
}
val profileId = SignalDatabase val profileId = SignalDatabase
.writableDatabase .writableDatabase
.insertInto(NotificationProfileTable.TABLE_NAME) .insertInto(NotificationProfileTable.TABLE_NAME)
@@ -50,7 +58,8 @@ object NotificationProfileProcessor {
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(), NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(),
NotificationProfileTable.CREATED_AT to profile.createdAtMs, NotificationProfileTable.CREATED_AT to profile.createdAtMs,
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(), NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt() NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(),
NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString()
) )
.run() .run()
@@ -89,6 +98,7 @@ object NotificationProfileProcessor {
private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame { private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame {
val profile = NotificationProfileProto( val profile = NotificationProfileProto(
id = UuidUtil.toByteArray(this.notificationProfileId.uuid).toByteString(),
name = this.name, name = this.name,
emoji = this.emoji.takeIf { it.isNotBlank() }, emoji = this.emoji.takeIf { it.isNotBlank() },
color = this.color.colorInt(), color = this.color.colorInt(),

View File

@@ -11,12 +11,14 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.requireBoolean import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt import org.signal.core.util.requireInt
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString import org.signal.core.util.requireString
import org.signal.core.util.toInt import org.signal.core.util.toInt
import org.signal.core.util.update import org.signal.core.util.update
import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import java.time.DayOfWeek import java.time.DayOfWeek
@@ -46,6 +48,7 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
const val CREATED_AT = "created_at" const val CREATED_AT = "created_at"
const val ALLOW_ALL_CALLS = "allow_all_calls" const val ALLOW_ALL_CALLS = "allow_all_calls"
const val ALLOW_ALL_MENTIONS = "allow_all_mentions" const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
val CREATE_TABLE = """ val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
@@ -55,7 +58,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
$COLOR TEXT NOT NULL, $COLOR TEXT NOT NULL,
$CREATED_AT INTEGER NOT NULL, $CREATED_AT INTEGER NOT NULL,
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0, $ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0 $ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0,
$NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL
) )
""" """
} }
@@ -110,12 +114,14 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
db.beginTransaction() db.beginTransaction()
try { try {
val notificationProfileId = NotificationProfileId.generate()
val profileValues = ContentValues().apply { val profileValues = ContentValues().apply {
put(NotificationProfileTable.NAME, name) put(NotificationProfileTable.NAME, name)
put(NotificationProfileTable.EMOJI, emoji) put(NotificationProfileTable.EMOJI, emoji)
put(NotificationProfileTable.COLOR, color.serialize()) put(NotificationProfileTable.COLOR, color.serialize())
put(NotificationProfileTable.CREATED_AT, createdAt) put(NotificationProfileTable.CREATED_AT, createdAt)
put(NotificationProfileTable.ALLOW_ALL_CALLS, 1) put(NotificationProfileTable.ALLOW_ALL_CALLS, 1)
put(NotificationProfileTable.NOTIFICATION_PROFILE_ID, notificationProfileId.serialize())
} }
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues) val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
@@ -140,7 +146,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
emoji = emoji, emoji = emoji,
createdAt = createdAt, createdAt = createdAt,
schedule = getProfileSchedule(profileId), schedule = getProfileSchedule(profileId),
allowAllCalls = true allowAllCalls = true,
notificationProfileId = notificationProfileId
) )
) )
} finally { } finally {
@@ -323,7 +330,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
allowAllCalls = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_CALLS), allowAllCalls = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_CALLS),
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS), allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
schedule = getProfileSchedule(profileId), schedule = getProfileSchedule(profileId),
allowedMembers = getProfileAllowedMembers(profileId) allowedMembers = getProfileAllowedMembers(profileId),
notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID))
) )
} }

View File

@@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V267_FixGroupInvita
import org.thoughtcrime.securesms.database.helpers.migration.V268_FixInAppPaymentsErrorStateConsistency import org.thoughtcrime.securesms.database.helpers.migration.V268_FixInAppPaymentsErrorStateConsistency
import org.thoughtcrime.securesms.database.helpers.migration.V269_BackupMediaSnapshotChanges import org.thoughtcrime.securesms.database.helpers.migration.V269_BackupMediaSnapshotChanges
import org.thoughtcrime.securesms.database.helpers.migration.V270_FixChatFolderColumnsForStorageSync import org.thoughtcrime.securesms.database.helpers.migration.V270_FixChatFolderColumnsForStorageSync
import org.thoughtcrime.securesms.database.helpers.migration.V271_AddNotificationProfileIdColumn
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/** /**
@@ -255,10 +256,11 @@ object SignalDatabaseMigrations {
267 to V267_FixGroupInvitationDeclinedUpdate, 267 to V267_FixGroupInvitationDeclinedUpdate,
268 to V268_FixInAppPaymentsErrorStateConsistency, 268 to V268_FixInAppPaymentsErrorStateConsistency,
269 to V269_BackupMediaSnapshotChanges, 269 to V269_BackupMediaSnapshotChanges,
270 to V270_FixChatFolderColumnsForStorageSync 270 to V270_FixChatFolderColumnsForStorageSync,
271 to V271_AddNotificationProfileIdColumn
) )
const val DATABASE_VERSION = 270 const val DATABASE_VERSION = 271
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.database.SQLiteDatabase
import java.util.UUID
/**
* Add notification_profile_id column to Notification Profiles to support backups.
*/
@Suppress("ClassName")
object V271_AddNotificationProfileIdColumn : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE notification_profile ADD COLUMN notification_profile_id TEXT DEFAULT NULL")
db.rawQuery("SELECT _id FROM notification_profile")
.readToList { it.requireLong("_id") }
.forEach { id ->
db.execSQL("UPDATE notification_profile SET notification_profile_id = '${UUID.randomUUID()}' WHERE _id = $id")
}
}
}

View File

@@ -12,7 +12,8 @@ data class NotificationProfile(
val allowAllCalls: Boolean = true, val allowAllCalls: Boolean = true,
val allowAllMentions: Boolean = false, val allowAllMentions: Boolean = false,
val schedule: NotificationProfileSchedule, val schedule: NotificationProfileSchedule,
val allowedMembers: Set<RecipientId> = emptySet() val allowedMembers: Set<RecipientId> = emptySet(),
val notificationProfileId: NotificationProfileId
) : Comparable<NotificationProfile> { ) : Comparable<NotificationProfile> {
fun isRecipientAllowed(id: RecipientId): Boolean { fun isRecipientAllowed(id: RecipientId): Boolean {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.notifications.profiles
import org.signal.core.util.DatabaseId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
/**
* Typed wrapper for notification profile uuid.
*/
data class NotificationProfileId(val uuid: UUID) : DatabaseId {
companion object {
fun from(id: String): NotificationProfileId {
return NotificationProfileId(UuidUtil.parseOrThrow(id))
}
fun generate(): NotificationProfileId {
return NotificationProfileId(UUID.randomUUID())
}
}
override fun serialize(): String {
return uuid.toString()
}
}

View File

@@ -724,11 +724,30 @@ message FilePointer {
message InvalidAttachmentLocator { message InvalidAttachmentLocator {
} }
// References attachments in a local encrypted backup.
// Importers should first attempt to read the file from the local backup,
// and on failure fallback to backup and transit cdn if possible.
message LocalLocator {
string mediaName = 1;
// Separate key used to encrypt this file for the local backup.
// Generally required. Missing field indicates attachment was not
// available locally when the backup was generated, but remote
// backup or transit info was available.
optional bytes localKey = 2;
bytes remoteKey = 3;
bytes remoteDigest = 4;
uint32 size = 5;
optional uint32 backupCdnNumber = 6;
optional string transitCdnKey = 7;
optional uint32 transitCdnNumber = 8;
}
// If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error. // If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error.
oneof locator { oneof locator {
BackupLocator backupLocator = 1; BackupLocator backupLocator = 1;
AttachmentLocator attachmentLocator = 2; AttachmentLocator attachmentLocator = 2;
InvalidAttachmentLocator invalidAttachmentLocator = 3; InvalidAttachmentLocator invalidAttachmentLocator = 3;
LocalLocator localLocator = 12;
} }
optional string contentType = 4; optional string contentType = 4;
@@ -1291,6 +1310,7 @@ message NotificationProfile {
uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
repeated DayOfWeek scheduleDaysEnabled = 11; repeated DayOfWeek scheduleDaysEnabled = 11;
bytes id = 12; // should be 16 bytes
} }
message ChatFolder { message ChatFolder {

View File

@@ -41,7 +41,8 @@ class NotificationProfilesTest {
"first", "first",
"", "",
createdAt = 1000L, createdAt = 1000L,
schedule = NotificationProfileSchedule(1) schedule = NotificationProfileSchedule(1),
notificationProfileId = NotificationProfileId.generate()
) )
private val second = NotificationProfile( private val second = NotificationProfile(
@@ -49,7 +50,8 @@ class NotificationProfilesTest {
"second", "second",
"", "",
createdAt = 2000L, createdAt = 2000L,
schedule = NotificationProfileSchedule(2) schedule = NotificationProfileSchedule(2),
notificationProfileId = NotificationProfileId.generate()
) )
private lateinit var notificationProfileValues: NotificationProfileValues private lateinit var notificationProfileValues: NotificationProfileValues