Signal-iOS/SignalServiceKit/Messages/Edit/EditMessageStore.swift
2024-05-20 14:14:14 -07:00

265 lines
8.0 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
import LibSignalClient
public enum EditMessageTarget {
case outgoingMessage(OutgoingEditMessageWrapper)
case incomingMessage(IncomingEditMessageWrapper)
var wrapper: any EditMessageWrapper {
switch self {
case .outgoingMessage(let outgoingMessage):
return outgoingMessage
case .incomingMessage(let incomingMessage):
return incomingMessage
}
}
}
public protocol EditMessageStore {
// MARK: - Reads
func editTarget(
timestamp: UInt64,
authorAci: Aci?,
tx: DBReadTransaction
) -> EditMessageTarget?
func findMessage(
fromEdit edit: TSMessage,
tx: DBReadTransaction
) -> TSMessage?
func numberOfEdits(
for message: TSMessage,
tx: DBReadTransaction
) -> Int
func findEditHistory(
for message: TSMessage,
tx: DBReadTransaction
) throws -> [(EditRecord, TSMessage?)]
/// This method is similar to findEditHistory, but will find records and interactions where the
/// passed in message is _either_ the latest edit, or a past revision. This is useful when
/// deleting a message, since the record needs to be removed regardles of the type of edit
func findEditDeleteRecords(
for message: TSMessage,
tx: DBReadTransaction
) throws -> [(EditRecord, TSMessage?)]
// MARK: - Writes
func insert(
_ editRecord: EditRecord,
tx: DBWriteTransaction
)
func update(
_ editRecord: EditRecord,
tx: DBWriteTransaction
) throws
}
public class EditMessageStoreImpl: EditMessageStore {
public init() {}
public func editTarget(
timestamp: UInt64,
authorAci: Aci?,
tx: DBReadTransaction
) -> EditMessageTarget? {
guard SDS.fitsInInt64(timestamp) else {
owsFailDebug("Received invalid timestamp!")
return nil
}
let transaction = SDSDB.shimOnlyBridge(tx)
let sql = """
SELECT *
FROM \(InteractionRecord.databaseTableName)
WHERE \(interactionColumn: .timestamp) = ?
AND \(interactionColumn: .authorUUID) IS ?
LIMIT 1
"""
let interaction = TSInteraction.grdbFetchOne(
sql: sql,
arguments: [timestamp, authorAci?.serviceIdUppercaseString],
transaction: transaction.unwrapGrdbRead
)
switch (interaction, authorAci) {
case (let outgoingMessage as TSOutgoingMessage, nil):
guard let thread = outgoingMessage.thread(tx: transaction) else {
Logger.warn("No thread for message")
return nil
}
return .outgoingMessage(OutgoingEditMessageWrapper(
message: outgoingMessage,
thread: thread
))
case (let incomingMessage as TSIncomingMessage, let authorAci?):
guard let thread = incomingMessage.thread(tx: transaction) else {
Logger.warn("No thread for message")
return nil
}
return .incomingMessage(IncomingEditMessageWrapper(
message: incomingMessage,
thread: thread,
authorAci: authorAci
))
case (.some, _):
Logger.warn("Unexpected message type found for edit")
fallthrough
default:
return nil
}
}
public func findMessage(
fromEdit edit: TSMessage,
tx: DBReadTransaction
) -> TSMessage? {
let transaction = SDSDB.shimOnlyBridge(tx)
let sql = """
SELECT * FROM \(InteractionRecord.databaseTableName) AS interaction
INNER JOIN \(EditRecord.databaseTableName) AS editRecord
ON interaction.\(interactionColumn: .id) = editRecord.latestRevisionId
WHERE editRecord.pastRevisionId = ?
LIMIT 1
"""
let arguments: StatementArguments = [edit.grdbId]
return TSMessage.grdbFetchOne(
sql: sql,
arguments: arguments,
transaction: transaction.unwrapGrdbRead
) as? TSMessage
}
public func numberOfEdits(
for message: TSMessage,
tx: DBReadTransaction
) -> Int {
let transaction = SDSDB.shimOnlyBridge(tx)
let sql = """
SELECT COUNT(*)
FROM \(EditRecord.databaseTableName)
WHERE editRecord.latestRevisionId = ?
"""
let arguments: StatementArguments = [message.grdbId]
do {
return try Int.fetchOne(
transaction.unwrapGrdbRead.database,
sql: sql,
arguments: arguments
) ?? 0
} catch {
DatabaseCorruptionState.flagDatabaseReadCorruptionIfNecessary(
userDefaults: CurrentAppContext().appUserDefaults(),
error: error
)
owsFail("Missing instance.")
}
}
public func findEditHistory(
for message: TSMessage,
tx: DBReadTransaction
) throws -> [(EditRecord, TSMessage?)] {
let transaction = SDSDB.shimOnlyBridge(tx)
let recordSQL = """
SELECT * FROM \(EditRecord.databaseTableName)
WHERE latestRevisionId = ?
ORDER BY pastRevisionId DESC
"""
let arguments: StatementArguments = [message.grdbId]
let records = try EditRecord.fetchAll(
transaction.unwrapGrdbRead.database,
sql: recordSQL,
arguments: arguments
)
return records.map { record -> (EditRecord, TSMessage?) in
let interaction = InteractionFinder.fetch(
rowId: record.pastRevisionId,
transaction: transaction
)
guard let message = interaction as? TSMessage else {
owsFailDebug("Interaction has unexpected type: \(type(of: interaction))")
return (record, nil)
}
return (record, message)
}
}
/// This method is similar to findEditHistory, but will find records and interactions where the
/// passed in message is _either_ the latest edit, or a past revision. This is useful when
/// deleting a message, since the record needs to be removed regardles of the type of edit
public func findEditDeleteRecords(
for message: TSMessage,
tx: DBReadTransaction
) throws -> [(EditRecord, TSMessage?)] {
let transaction = SDSDB.shimOnlyBridge(tx)
let recordSQL = """
SELECT * FROM \(EditRecord.databaseTableName)
WHERE latestRevisionId = ?
OR pastRevisionId = ?
ORDER BY pastRevisionId DESC
"""
let arguments: StatementArguments = [message.grdbId, message.grdbId]
let records = try EditRecord.fetchAll(
transaction.unwrapGrdbRead.database,
sql: recordSQL,
arguments: arguments
)
return records.map { record -> (EditRecord, TSMessage?) in
let interaction = InteractionFinder.fetch(
rowId: record.pastRevisionId,
transaction: transaction
)
guard let message = interaction as? TSMessage else {
owsFailDebug("Interaction has unexpected type: \(type(of: interaction))")
return (record, nil)
}
return (record, message)
}
}
public func insert(
_ editRecord: EditRecord,
tx: DBWriteTransaction
) {
do {
try editRecord.insert(SDSDB.shimOnlyBridge(tx).unwrapGrdbWrite.database)
} catch {
owsFailDebug("Unexpected edit record insertion error \(error)")
}
}
public func update(
_ editRecord: EditRecord,
tx: DBWriteTransaction
) throws {
try editRecord.update(SDSDB.shimOnlyBridge(tx).unwrapGrdbWrite.database)
}
}