Never allow My Story to be deleted

This commit is contained in:
Sasha Weiss 2026-06-01 12:42:17 -07:00 committed by GitHub
parent 185035784c
commit f64e718ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 50 additions and 10 deletions

View File

@ -14,13 +14,20 @@ public class StoryManager {
cacheAreViewReceiptsEnabled()
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
SSKEnvironment.shared.databaseStorageRef.asyncWrite { transaction in
// Create My Story thread if necessary
TSPrivateStoryThread.getOrCreateMyStory(transaction: transaction)
let db = DependenciesBridge.shared.db
let storageServiceManager = SSKEnvironment.shared.storageServiceManagerRef
let privateStoryThreadDeletionManager = DependenciesBridge.shared.privateStoryThreadDeletionManager
db.asyncWrite { tx in
if TSPrivateStoryThread.getMyStory(transaction: tx) == nil {
let myStory = TSPrivateStoryThread.getOrCreateMyStory(transaction: tx)
storageServiceManager.recordPendingUpdates(
updatedStoryDistributionListIds: [myStory.distributionListIdentifier!],
)
}
if CurrentAppContext().isMainApp {
DependenciesBridge.shared.privateStoryThreadDeletionManager
.cleanUpDeletedTimestamps(tx: transaction)
privateStoryThreadDeletionManager.cleanUpDeletedTimestamps(tx: tx)
}
}
}

View File

@ -331,6 +331,7 @@ public class GRDBSchemaMigrator {
case migrateSecureValueRecovery
case wipeCachedSVRBAuthCredentials
case addMimeTypeToMessageAttachmentReference
case purgeMyStoryDeletedAtTimestamp
// NOTE: Every time we add a migration id, consider
// incrementing grdbSchemaVersionLatest.
@ -454,7 +455,7 @@ public class GRDBSchemaMigrator {
}
public static let grdbSchemaVersionDefault: UInt = 0
public static let grdbSchemaVersionLatest: UInt = 143
public static let grdbSchemaVersionLatest: UInt = 144
private class DatabaseMigratorWrapper {
// Run with immediate (or disabled) foreign key checks so that pre-existing
@ -5184,6 +5185,24 @@ public class GRDBSchemaMigrator {
return .success(())
}
migrator.registerMigration(.purgeMyStoryDeletedAtTimestamp) { tx in
// "My Story" should never be deleted, but we've seen reports where
// it has been marked as such. Remove it from the deleted-story
// store if necessary; we (at the time of writing) create the My Story
// thread on app launch and have guards against deleting it again in
// the future.
try tx.database.execute(
sql: """
DELETE FROM keyvalue WHERE collection = ? AND key = ?
""",
arguments: [
"TSPrivateStoryThread+DeletedAtTimestamp",
"00000000-0000-0000-0000-000000000000",
],
)
return .success(())
}
// MARK: - Schema Migration Insertion Point
}

View File

@ -2077,6 +2077,12 @@ class StorageServiceStoryDistributionListRecordUpdater: StorageServiceRecordUpda
// The story has been deleted on another device, record that
// and ensure we don't try and put it back.
guard record.deletedAtTimestamp == 0 else {
if uniqueId.uuidString == TSPrivateStoryThread.myStoryUniqueId {
// My Story should never be deleted. If someone put such in
// Storage Service, ignore and overwrite it.
return .merged(needsUpdate: true, identifier)
}
if let existingStory {
threadRemover.remove(existingStory, tx: transaction)
}

View File

@ -84,7 +84,15 @@ final class PrivateStoryThreadDeletionManagerImpl: PrivateStoryThreadDeletionMan
return
}
guard let uniqueId = identifier.uuidString else { return }
guard let uniqueId = identifier.uuidString else {
return
}
if uniqueId == TSPrivateStoryThread.myStoryUniqueId {
owsFailDebug("Refusing to record My Story as deleted.", logger: logger)
return
}
deletedAtTimestampStore.setUInt64(timestamp, key: uniqueId, transaction: tx)
}

View File

@ -145,7 +145,7 @@ public final class TSPrivateStoryThread: TSThread {
"00000000-0000-0000-0000-000000000000"
}
public class func getMyStory(transaction: DBReadTransaction) -> TSPrivateStoryThread! {
public class func getMyStory(transaction: DBReadTransaction) -> TSPrivateStoryThread? {
fetchPrivateStoryThreadViaCache(uniqueId: myStoryUniqueId, transaction: transaction)
}

View File

@ -82,7 +82,7 @@ private class MyStorySettingsDataSource: NSObject {
myStoryThread,
myStoryThreadRecipientIds,
) = databaseStorage.read { transaction -> (Bool, TSPrivateStoryThread, [SignalRecipient.RowId]) in
let myStoryThread: TSPrivateStoryThread = TSPrivateStoryThread.getMyStory(transaction: transaction)
let myStoryThread: TSPrivateStoryThread = TSPrivateStoryThread.getMyStory(transaction: transaction)!
return (
StoryManager.hasSetMyStoriesPrivacy(transaction: transaction),
myStoryThread,
@ -249,7 +249,7 @@ private class MyStorySettingsDataSource: NSObject {
@objc
private func didToggleReplies(_ toggle: UISwitch) {
let myStoryThread: TSPrivateStoryThread! = SSKEnvironment.shared.databaseStorageRef.read { TSPrivateStoryThread.getMyStory(transaction: $0) }
let myStoryThread: TSPrivateStoryThread = SSKEnvironment.shared.databaseStorageRef.read { TSPrivateStoryThread.getMyStory(transaction: $0)! }
guard myStoryThread.allowsReplies != toggle.isOn else { return }
SSKEnvironment.shared.databaseStorageRef.write { transaction in
myStoryThread.updateWithAllowsReplies(toggle.isOn, updateStorageService: true, transaction: transaction)