298 lines
11 KiB
Swift
298 lines
11 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
public import LibSignalClient
|
|
|
|
@objc
|
|
public class ViewOnceMessages: NSObject {
|
|
|
|
public init(appReadiness: AppReadiness) {
|
|
super.init()
|
|
|
|
if CurrentAppContext().isMainApp {
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
Self.appDidBecomeReady()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
private class func nowMs() -> UInt64 {
|
|
return NSDate.ows_millisecondTimeStamp()
|
|
}
|
|
|
|
private class func appDidBecomeReady() {
|
|
AssertIsOnMainThread()
|
|
|
|
DispatchQueue.global().async {
|
|
self.checkForAutoCompletion()
|
|
}
|
|
}
|
|
|
|
// "Check for auto-completion", e.g. complete messages whether or
|
|
// not they have been read after N days. Also complete outgoing
|
|
// sent messages. We need to repeat this check periodically while
|
|
// the app is running.
|
|
private class func checkForAutoCompletion() {
|
|
// Find all view-once messages which are not yet complete.
|
|
// Complete messages if necessary.
|
|
SSKEnvironment.shared.databaseStorageRef.write { (transaction) in
|
|
let messages = ViewOnceMessageFinder()
|
|
.allMessagesWithViewOnceMessage(transaction: transaction)
|
|
for message in messages {
|
|
completeIfNecessary(message: message, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// We need to "check for auto-completion" once per day.
|
|
DispatchQueue.global().asyncAfter(wallDeadline: .now() + kDayInterval) {
|
|
self.checkForAutoCompletion()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public class func completeIfNecessary(message: TSMessage,
|
|
transaction: SDSAnyWriteTransaction) {
|
|
|
|
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: SDSAnyWriteTransaction) {
|
|
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: SDSAnyWriteTransaction) {
|
|
guard let senderAddress = senderAddress(forMessage: message) else {
|
|
owsFailDebug("Could not send sync message; no local number.")
|
|
return
|
|
}
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: transaction) else {
|
|
owsFailDebug("Missing thread.")
|
|
return
|
|
}
|
|
let readTimestamp: UInt64 = nowMs()
|
|
|
|
let syncMessage = OWSViewOnceMessageReadSyncMessage(
|
|
thread: thread,
|
|
senderAddress: senderAddress,
|
|
message: message,
|
|
readTimestamp: readTimestamp,
|
|
transaction: 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: SDSAnyWriteTransaction
|
|
) -> ViewOnceSyncMessageProcessingResult {
|
|
guard let messageSender = Aci.parseFrom(aciString: 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) -> Bool in
|
|
guard interaction.timestamp == messageIdTimestamp else {
|
|
owsFailDebug("Timestamps don't match: \(interaction.timestamp) != \(messageIdTimestamp)")
|
|
return false
|
|
}
|
|
guard let message = interaction as? TSMessage else {
|
|
return false
|
|
}
|
|
guard let senderAddress = senderAddress(forMessage: message) else {
|
|
owsFailDebug("Could not process sync message; no local number.")
|
|
return false
|
|
}
|
|
guard senderAddress.serviceId == messageSender else {
|
|
return false
|
|
}
|
|
guard message.isViewOnceMessage else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
let interactions: [TSInteraction]
|
|
do {
|
|
interactions = try InteractionFinder.interactions(withTimestamp: messageIdTimestamp, filter: filter, transaction: transaction)
|
|
} catch {
|
|
owsFailDebug("Couldn't find interactions: \(error)")
|
|
return .invalidSyncMessage
|
|
}
|
|
guard interactions.count > 0 else {
|
|
return .associatedMessageMissing(senderAci: messageSender, associatedMessageTimestamp: messageIdTimestamp)
|
|
}
|
|
if interactions.count > 1 {
|
|
owsFailDebug("More than one message from the same sender with the same timestamp found.")
|
|
}
|
|
for interaction in interactions {
|
|
guard let message = interaction as? TSMessage else {
|
|
owsFailDebug("Invalid interaction: \(type(of: interaction))")
|
|
continue
|
|
}
|
|
// Mark as complete.
|
|
markAsComplete(message: message,
|
|
sendSyncMessages: false,
|
|
transaction: transaction)
|
|
}
|
|
return .success
|
|
}
|
|
|
|
private class func senderAddress(forMessage message: TSMessage) -> SignalServiceAddress? {
|
|
|
|
if let incomingMessage = message as? TSIncomingMessage {
|
|
return incomingMessage.authorAddress
|
|
} else if message is TSOutgoingMessage {
|
|
guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress 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 localAddress
|
|
} else {
|
|
owsFailDebug("Unexpected message type.")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private class ViewOnceMessageFinder {
|
|
public func allMessagesWithViewOnceMessage(transaction: SDSAnyReadTransaction) -> [TSMessage] {
|
|
var result: [TSMessage] = []
|
|
self.enumerateAllIncompleteViewOnceMessages(transaction: transaction) { message in
|
|
result.append(message)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func enumerateAllIncompleteViewOnceMessages(
|
|
transaction: SDSAnyReadTransaction,
|
|
block: (TSMessage) -> Void
|
|
) {
|
|
let sql = """
|
|
SELECT *
|
|
FROM \(InteractionRecord.databaseTableName)
|
|
WHERE \(interactionColumn: .isViewOnceMessage) IS NOT NULL
|
|
AND \(interactionColumn: .isViewOnceMessage) == TRUE
|
|
AND \(interactionColumn: .isViewOnceComplete) IS NOT NULL
|
|
AND \(interactionColumn: .isViewOnceComplete) == FALSE
|
|
"""
|
|
let cursor = TSInteraction.grdbFetchCursor(
|
|
sql: sql,
|
|
transaction: transaction.unwrapGrdbRead
|
|
)
|
|
|
|
// GRDB TODO make cursor.next fail hard to remove this `try!`
|
|
while let next = try! cursor.next() {
|
|
guard let message = next as? TSMessage else {
|
|
owsFailDebug("expecting message but found: \(next)")
|
|
return
|
|
}
|
|
|
|
guard
|
|
message.isViewOnceMessage,
|
|
!message.isViewOnceComplete
|
|
else {
|
|
owsFailDebug("expecting incomplete view-once message but found: \(message)")
|
|
return
|
|
}
|
|
|
|
block(message)
|
|
}
|
|
}
|
|
}
|