299 lines
10 KiB
Swift
299 lines
10 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
public import LibSignalClient
|
|
|
|
@objc
|
|
public class ViewOnceMessages: NSObject {
|
|
|
|
override private init() {
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
private class func nowMs() -> UInt64 {
|
|
return NSDate.ows_millisecondTimeStamp()
|
|
}
|
|
|
|
// "Check for auto-completion", e.g. complete messages whether or
|
|
// not they have been read after N days. Also complete outgoing
|
|
// sent messages.
|
|
public static func expireIfNecessary() async throws(CancellationError) {
|
|
// Find all view-once messages which are not yet complete.
|
|
// Complete messages if necessary.
|
|
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
|
var afterRowId: Int64?
|
|
repeat {
|
|
if Task.isCancelled {
|
|
throw CancellationError()
|
|
}
|
|
await databaseStorage.awaitableWrite { tx in
|
|
let messages: [TSMessage]
|
|
(messages, afterRowId) = ViewOnceMessageFinder().fetchSomeIncompleteViewOnceMessages(after: afterRowId, limit: 100, tx: tx)
|
|
if !messages.isEmpty {
|
|
Logger.info("Checking \(messages.count) view once message(s) for auto-expiration.")
|
|
}
|
|
for message in messages {
|
|
completeIfNecessary(message: message, transaction: tx)
|
|
}
|
|
}
|
|
} while afterRowId != nil
|
|
}
|
|
|
|
@objc
|
|
public class func completeIfNecessary(
|
|
message: TSMessage,
|
|
transaction: DBWriteTransaction,
|
|
) {
|
|
|
|
guard
|
|
message.isViewOnceMessage,
|
|
!message.isViewOnceComplete
|
|
else {
|
|
return
|
|
}
|
|
|
|
// If message should auto-complete, complete.
|
|
guard !shouldMessageAutoComplete(message) else {
|
|
markAsComplete(
|
|
message: message,
|
|
sendSyncMessages: true,
|
|
transaction: transaction,
|
|
)
|
|
return
|
|
}
|
|
|
|
// If outgoing message and is "sent", complete.
|
|
guard !isOutgoingSent(message: message) else {
|
|
markAsComplete(
|
|
message: message,
|
|
sendSyncMessages: true,
|
|
transaction: transaction,
|
|
)
|
|
return
|
|
}
|
|
|
|
// Message should not yet complete.
|
|
}
|
|
|
|
private class func isOutgoingSent(message: TSMessage) -> Bool {
|
|
guard message.isViewOnceMessage else {
|
|
owsFailDebug("Unexpected message.")
|
|
return false
|
|
}
|
|
// If outgoing message and is "sent", complete.
|
|
guard let outgoingMessage = message as? TSOutgoingMessage else {
|
|
return false
|
|
}
|
|
guard outgoingMessage.messageState == .sent else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// We auto-complete messages after N days, even if the user hasn't seen them.
|
|
private class func shouldMessageAutoComplete(_ message: TSMessage) -> Bool {
|
|
let autoCompleteDeadlineMs = min(message.timestamp, message.receivedAtTimestamp) + RemoteConfig.current.messageQueueTimeMs
|
|
return nowMs() >= autoCompleteDeadlineMs
|
|
}
|
|
|
|
@objc
|
|
public class func markAsComplete(
|
|
message: TSMessage,
|
|
sendSyncMessages: Bool,
|
|
transaction: DBWriteTransaction,
|
|
) {
|
|
guard message.isViewOnceMessage else {
|
|
owsFailDebug("Not a view-once message.")
|
|
return
|
|
}
|
|
guard !message.isViewOnceComplete else {
|
|
// Already completed, no need to complete again.
|
|
return
|
|
}
|
|
message.updateWithViewOnceCompleteAndRemoveRenderableContent(with: transaction)
|
|
|
|
if sendSyncMessages {
|
|
sendSyncMessage(forMessage: message, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: - Sync Messages
|
|
|
|
private class func sendSyncMessage(forMessage message: TSMessage, transaction: DBWriteTransaction) {
|
|
guard let senderAci = senderAci(forMessage: message, tx: transaction) else {
|
|
return
|
|
}
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: transaction) else {
|
|
owsFailDebug("Missing thread.")
|
|
return
|
|
}
|
|
let readTimestamp: UInt64 = nowMs()
|
|
|
|
let syncMessage = OutgoingViewOnceOpenSyncMessage(
|
|
localThread: thread,
|
|
senderAci: senderAci,
|
|
message: message,
|
|
readTimestamp: readTimestamp,
|
|
tx: transaction,
|
|
)
|
|
// this is the sync that we viewed; it doesn't have the attachment on it.
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: syncMessage,
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
|
|
|
|
if let incomingMessage = message as? TSIncomingMessage {
|
|
let circumstance: OWSReceiptCircumstance =
|
|
thread.hasPendingMessageRequest(transaction: transaction)
|
|
? .onThisDeviceWhilePendingMessageRequest
|
|
: .onThisDevice
|
|
incomingMessage.markAsViewed(
|
|
atTimestamp: readTimestamp,
|
|
thread: thread,
|
|
circumstance: circumstance,
|
|
transaction: transaction,
|
|
)
|
|
}
|
|
}
|
|
|
|
public enum ViewOnceSyncMessageProcessingResult {
|
|
case associatedMessageMissing(senderAci: Aci, associatedMessageTimestamp: UInt64)
|
|
case invalidSyncMessage
|
|
case success
|
|
}
|
|
|
|
public class func processIncomingSyncMessage(
|
|
_ message: SSKProtoSyncMessageViewOnceOpen,
|
|
envelope: SSKProtoEnvelope,
|
|
transaction: DBWriteTransaction,
|
|
) -> ViewOnceSyncMessageProcessingResult {
|
|
guard
|
|
let messageSender = Aci.parseFrom(
|
|
serviceIdBinary: message.senderAciBinary,
|
|
serviceIdString: message.senderAci,
|
|
)
|
|
else {
|
|
owsFailDebug("Invalid messageSender.")
|
|
return .invalidSyncMessage
|
|
}
|
|
let messageIdTimestamp: UInt64 = message.timestamp
|
|
guard messageIdTimestamp > 0, SDS.fitsInInt64(messageIdTimestamp) else {
|
|
owsFailDebug("Invalid messageIdTimestamp.")
|
|
return .invalidSyncMessage
|
|
}
|
|
|
|
let filter = { (interaction: TSInteraction) -> TSMessage? in
|
|
guard let message = interaction as? TSMessage else {
|
|
return nil
|
|
}
|
|
guard let senderAci = senderAci(forMessage: message, tx: transaction) else {
|
|
return nil
|
|
}
|
|
guard senderAci == messageSender else {
|
|
return nil
|
|
}
|
|
guard message.isViewOnceMessage else {
|
|
return nil
|
|
}
|
|
return message
|
|
}
|
|
let messages: [TSMessage]
|
|
do {
|
|
messages = try InteractionFinder.fetchInteractions(
|
|
timestamp: messageIdTimestamp,
|
|
transaction: transaction,
|
|
).compactMap(filter)
|
|
} catch {
|
|
owsFailDebug("Couldn't find interactions: \(error)")
|
|
return .invalidSyncMessage
|
|
}
|
|
guard messages.count > 0 else {
|
|
return .associatedMessageMissing(senderAci: messageSender, associatedMessageTimestamp: messageIdTimestamp)
|
|
}
|
|
if messages.count > 1 {
|
|
owsFailDebug("More than one message from the same sender with the same timestamp found.")
|
|
}
|
|
for message in messages {
|
|
// Mark as complete.
|
|
markAsComplete(
|
|
message: message,
|
|
sendSyncMessages: false,
|
|
transaction: transaction,
|
|
)
|
|
}
|
|
return .success
|
|
}
|
|
|
|
private static func senderAci(forMessage message: TSMessage, tx: DBReadTransaction) -> Aci? {
|
|
if let incomingMessage = message as? TSIncomingMessage {
|
|
return incomingMessage.authorAddress.aci
|
|
} else if message is TSOutgoingMessage {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) else {
|
|
owsFailDebug("Could not process sync message; no local number.")
|
|
return nil
|
|
}
|
|
// We also need to send and receive "per-message expiration read" sync
|
|
// messages for outgoing messages, unlike normal read receipts.
|
|
return localIdentifiers.aci
|
|
} else {
|
|
owsFailDebug("Unexpected message type.")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private class ViewOnceMessageFinder {
|
|
func fetchSomeIncompleteViewOnceMessages(after rowId: Int64?, limit: Int, tx: DBReadTransaction) -> ([TSMessage], mightHaveMoreAfter: Int64?) {
|
|
var results: [TSMessage] = []
|
|
|
|
let cursor: TSInteractionCursor
|
|
if let rowId {
|
|
cursor = TSInteraction.grdbFetchCursor(
|
|
sql: """
|
|
SELECT *
|
|
FROM \(InteractionRecord.databaseTableName)
|
|
\(DEBUG_INDEXED_BY("Interaction_incompleteViewOnce_partial"))
|
|
WHERE \(interactionColumn: .isViewOnceMessage) = 1
|
|
AND \(interactionColumn: .isViewOnceComplete) = 0
|
|
AND \(interactionColumn: .id) > ?
|
|
ORDER BY \(interactionColumn: .id)
|
|
""",
|
|
arguments: [rowId],
|
|
transaction: tx,
|
|
)
|
|
} else {
|
|
cursor = TSInteraction.grdbFetchCursor(
|
|
sql: """
|
|
SELECT *
|
|
FROM \(InteractionRecord.databaseTableName)
|
|
\(DEBUG_INDEXED_BY("Interaction_incompleteViewOnce_partial"))
|
|
WHERE \(interactionColumn: .isViewOnceMessage) = 1
|
|
AND \(interactionColumn: .isViewOnceComplete) = 0
|
|
ORDER BY \(interactionColumn: .id)
|
|
""",
|
|
transaction: tx,
|
|
)
|
|
}
|
|
|
|
while let next = try! cursor.next() {
|
|
guard let message = next as? TSMessage else {
|
|
owsFailDebug("expecting message but found: \(next)")
|
|
continue
|
|
}
|
|
results.append(message)
|
|
if results.count >= limit {
|
|
return (results, mightHaveMoreAfter: message.sqliteRowId!)
|
|
}
|
|
}
|
|
return (results, mightHaveMoreAfter: nil)
|
|
}
|
|
}
|