Do not include release notes in chat exports.

This commit is contained in:
Greyson Parrelli
2025-12-11 14:07:18 -05:00
parent 10133b16b3
commit e930a0f8ac
24 changed files with 266 additions and 46 deletions

View File

@@ -66,11 +66,6 @@ class ArchiveImportExportTests {
runTests { it.matches(Regex("^chat_%d%d.binproto$")) }
}
// @Test
fun chatReleaseNotes() {
runTests { it.startsWith("chat_release_notes_") }
}
// @Test
fun chatFolders() {
runTests { it.startsWith("chat_folder_") }

View File

@@ -5,19 +5,17 @@
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.SqlUtil
import org.signal.core.util.forEach
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.exporters.ChatArchiveExporter
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
fun ThreadTable.getThreadsForBackup(db: SignalDatabase, exportState: ExportState, includeImageWallpapers: Boolean): ChatArchiveExporter {
val notReleaseNoteClause = exportState.releaseNoteRecipientId?.let {
"AND ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} != $it"
} ?: ""
//language=sql
val query = """
SELECT
@@ -37,39 +35,9 @@ fun ThreadTable.getThreadsForBackup(db: SignalDatabase, exportState: ExportState
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE
${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} NOT IN (${RecipientTable.RecipientType.DISTRIBUTION_LIST.id}, ${RecipientTable.RecipientType.CALL_LINK.id})
$notReleaseNoteClause
"""
val cursor = readableDatabase.query(query)
return ChatArchiveExporter(cursor, db, exportState, includeImageWallpapers)
}
fun ThreadTable.getThreadGroupStatus(messageIds: Collection<Long>): Map<Long, Boolean> {
if (messageIds.isEmpty()) {
return emptyMap()
}
val out: MutableMap<Long, Boolean> = mutableMapOf()
val query = SqlUtil.buildFastCollectionQuery("${MessageTable.TABLE_NAME}.${MessageTable.ID}", messageIds)
readableDatabase
.select(
"${MessageTable.TABLE_NAME}.${MessageTable.ID}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE}"
)
.from(
"""
${MessageTable.TABLE_NAME}
INNER JOIN ${ThreadTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
INNER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
"""
)
.where(query.where, query.whereArgs)
.run()
.forEach { cursor ->
val messageId = cursor.requireLong(MessageTable.ID)
val type = cursor.requireInt(RecipientTable.TYPE)
out[messageId] = type != RecipientTable.RecipientType.INDIVIDUAL.id
}
return out
}

View File

@@ -182,7 +182,7 @@ class ChatItemArchiveExporter(
val builder = record.toBasicChatItemBuilder(selfRecipientId, extraData.groupReceiptsById[id], exportState, backupStartTime)
transformTimer.emit("basic")
if (builder == null) {
if (builder == null || builder.authorId == exportState.releaseNoteRecipientId) {
continue
}
@@ -1639,7 +1639,7 @@ private fun ChatItem.validateChatItem(exportState: ExportState, selfRecipientId:
return null
}
if (this.incoming != null && this.authorId != exportState.releaseNoteRecipientId && exportState.recipientIdToAci[this.authorId] == null && exportState.recipientIdToE164[this.authorId] == null) {
if (this.incoming != null && exportState.recipientIdToAci[this.authorId] == null && exportState.recipientIdToE164[this.authorId] == null) {
Log.w(TAG, ExportSkips.incomingMessageAuthorDoesNotHaveAciOrE164(this.dateSent))
return null
}

View File

@@ -3168,6 +3168,10 @@ class AttachmentTable(
}
private fun buildAttachmentsThatNeedUploadQuery(transferStateFilter: String = "$ARCHIVE_TRANSFER_STATE IN (${ArchiveTransferState.NONE.value}, ${ArchiveTransferState.TEMPORARY_FAILURE.value})"): String {
val notReleaseChannelClause = SignalStore.releaseChannel.releaseChannelRecipientId?.let {
"(${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} != ${it.toLong()}) AND"
} ?: ""
return """
$transferStateFilter AND
$DATA_FILE NOT NULL AND
@@ -3176,6 +3180,7 @@ class AttachmentTable(
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
(${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND
(${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 OR ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} > ${ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds}) AND
$notReleaseChannelClause
$CONTENT_TYPE != '${MediaUtil.LONG_TEXT}' AND
${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} = 0
"""

View File

@@ -151,6 +151,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V294_RemoveLastReso
import org.thoughtcrime.securesms.database.helpers.migration.V295_AddLastRestoreKeyTypeTableIfMissingMigration
import org.thoughtcrime.securesms.database.helpers.migration.V296_RemovePollVoteConstraint
import org.thoughtcrime.securesms.database.helpers.migration.V297_AddPinnedMessageColumns
import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupReleaseNotes
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -308,10 +309,11 @@ object SignalDatabaseMigrations {
294 to V294_RemoveLastResortKeyTupleColumnConstraintMigration,
295 to V295_AddLastRestoreKeyTypeTableIfMissingMigration,
296 to V296_RemovePollVoteConstraint,
297 to V297_AddPinnedMessageColumns
297 to V297_AddPinnedMessageColumns,
298 to V298_DoNotBackupReleaseNotes
)
const val DATABASE_VERSION = 297
const val DATABASE_VERSION = 298
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,52 @@
/*
* 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.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SQLiteDatabase
object V298_DoNotBackupReleaseNotes : SignalDatabaseMigration {
private val TAG = Log.tag(V298_DoNotBackupReleaseNotes::class.java)
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val releaseNoteRecipientId = getReleaseNoteRecipientId(context) ?: return
migrateWithRecipientId(db, releaseNoteRecipientId)
}
fun migrateWithRecipientId(db: SQLiteDatabase, releaseNoteRecipientId: Long) {
db.execSQL(
"""
UPDATE attachment
SET archive_transfer_state = 0
WHERE message_id IN (
SELECT _id FROM message WHERE from_recipient_id = $releaseNoteRecipientId
)
"""
)
}
private fun getReleaseNoteRecipientId(context: Application): Long? {
return if (KeyValueDatabase.exists(context)) {
val keyValueDatabase = KeyValueDatabase.getInstance(context).readableDatabase
keyValueDatabase.query("key_value", arrayOf("value"), "key = ?", SqlUtil.buildArgs("releasechannel.recipient_id"), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
cursor.requireLong("value")
} else {
Log.w(TAG, "Release note channel recipient ID not found in KV database!")
null
}
}
} else {
Log.w(TAG, "Pre-KV database, not doing anything.")
null
}
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import android.content.ContentValues
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule
@Suppress("ClassName")
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class V298_DoNotBackupReleaseNotesTest {
@get:Rule val signalDatabaseRule = SignalDatabaseMigrationRule(297)
private val releaseNoteRecipientId = 100L
private val otherRecipientId = 200L
@Test
fun `migrate - attachments from release note recipient - clears archive transfer state`() {
val messageId = insertMessage(fromRecipientId = releaseNoteRecipientId)
val attachmentId = insertAttachment(
messageId = messageId,
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
archiveCdn = 2
)
runMigration()
assertArchiveState(attachmentId, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
}
@Test
fun `migrate - attachments from other recipient - no change`() {
val messageId = insertMessage(fromRecipientId = otherRecipientId)
val attachmentId = insertAttachment(
messageId = messageId,
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
archiveCdn = 2
)
runMigration()
assertArchiveState(attachmentId, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value)
}
@Test
fun `migrate - multiple attachments from release note recipient - all cleared`() {
val messageId1 = insertMessage(fromRecipientId = releaseNoteRecipientId)
val messageId2 = insertMessage(fromRecipientId = releaseNoteRecipientId)
val attachmentId1 = insertAttachment(
messageId = messageId1,
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
archiveCdn = 2
)
val attachmentId2 = insertAttachment(
messageId = messageId2,
archiveTransferState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value,
archiveCdn = 3
)
runMigration()
assertArchiveState(attachmentId1, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
assertArchiveState(attachmentId2, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
}
@Test
fun `migrate - mixed recipients - only release note attachments cleared`() {
val releaseNoteMessageId = insertMessage(fromRecipientId = releaseNoteRecipientId)
val otherMessageId = insertMessage(fromRecipientId = otherRecipientId)
val releaseNoteAttachmentId = insertAttachment(
messageId = releaseNoteMessageId,
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
archiveCdn = 2
)
val otherAttachmentId = insertAttachment(
messageId = otherMessageId,
archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value,
archiveCdn = 2
)
runMigration()
assertArchiveState(releaseNoteAttachmentId, expectedState = AttachmentTable.ArchiveTransferState.NONE.value)
assertArchiveState(otherAttachmentId, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value)
}
private fun runMigration() {
V298_DoNotBackupReleaseNotes.migrateWithRecipientId(
db = signalDatabaseRule.database,
releaseNoteRecipientId = releaseNoteRecipientId
)
}
private val insertedRecipients = mutableSetOf<Long>()
private val insertedThreads = mutableMapOf<Long, Long>()
private var dateSentCounter = System.currentTimeMillis()
private fun insertRecipient(recipientId: Long): Long {
if (recipientId in insertedRecipients) {
return recipientId
}
val db = signalDatabaseRule.database
val values = ContentValues().apply {
put("_id", recipientId)
put("type", 0)
}
db.insert("recipient", null, values)
insertedRecipients.add(recipientId)
return recipientId
}
private fun insertThread(recipientId: Long): Long {
insertedThreads[recipientId]?.let { return it }
val db = signalDatabaseRule.database
val values = ContentValues().apply {
put("recipient_id", recipientId)
put("date", System.currentTimeMillis())
}
val threadId = db.insert("thread", null, values)
insertedThreads[recipientId] = threadId
return threadId
}
private fun insertMessage(fromRecipientId: Long): Long {
val db = signalDatabaseRule.database
insertRecipient(fromRecipientId)
val threadId = insertThread(fromRecipientId)
dateSentCounter++
val values = ContentValues().apply {
put("date_sent", dateSentCounter)
put("date_received", System.currentTimeMillis())
put("thread_id", threadId)
put("from_recipient_id", fromRecipientId)
put("to_recipient_id", fromRecipientId)
put("type", 0)
}
return db.insert("message", null, values)
}
private fun insertAttachment(messageId: Long, archiveTransferState: Int, archiveCdn: Int?): Long {
val db = signalDatabaseRule.database
val values = ContentValues().apply {
put("message_id", messageId)
put("data_file", "/fake/path/attachment.jpg")
put("data_random", "/fake/path/attachment.jpg".toByteArray())
put("transfer_state", 0)
put("archive_transfer_state", archiveTransferState)
if (archiveCdn != null) {
put("archive_cdn", archiveCdn)
}
}
return db.insert("attachment", null, values)
}
private fun assertArchiveState(attachmentId: Long, expectedState: Int) {
val db = signalDatabaseRule.database
val cursor = db.query(
"attachment",
arrayOf("archive_transfer_state"),
"_id = ?",
arrayOf(attachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.getInt(it.getColumnIndexOrThrow("archive_transfer_state"))).isEqualTo(expectedState)
}
}
}