// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import LibSignalClient public class MessageRequestPendingReceipts: Dependencies, PendingReceiptRecorder { public init() { AppReadiness.runNowOrWhenAppDidBecomeReadyAsync { NotificationCenter.default.addObserver(self, selector: #selector(self.profileWhitelistDidChange(notification:)), name: UserProfileNotifications.profileWhitelistDidChange, object: nil) DispatchQueue.global().async { self.auditPendingReceipts() } } } // MARK: - let finder = PendingReceiptFinder() // MARK: - public func recordPendingReadReceipt(for message: TSIncomingMessage, thread: TSThread, transaction: GRDBWriteTransaction) { do { try finder.recordPendingReadReceipt(for: message, thread: thread, transaction: transaction) } catch { owsFailDebug("error: \(error)") } } public func recordPendingViewedReceipt(for message: TSIncomingMessage, thread: TSThread, transaction: GRDBWriteTransaction) { do { try finder.recordPendingViewedReceipt(for: message, thread: thread, transaction: transaction) } catch { owsFailDebug("error: \(error)") } } // MARK: - @objc private func profileWhitelistDidChange(notification: Notification) { do { try grdbStorageAdapter.read { transaction in guard let thread = notification.affectedThread(transaction: transaction) else { return } let userProfileWriter = notification.userProfileWriter if userProfileWriter == .localUser { try self.sendAnyReadyReceipts(threads: [thread], transaction: transaction) } else { try self.removeAnyReadyReceipts(threads: [thread], transaction: transaction) } } } catch { owsFailDebug("error: \(error)") } } private func auditPendingReceipts() { do { try grdbStorageAdapter.read { transaction in let threads = try self.finder.threadsWithPendingReceipts(transaction: transaction) try self.sendAnyReadyReceipts(threads: threads, transaction: transaction) } } catch { owsFailDebug("error: \(error)") } } private func sendAnyReadyReceipts(threads: [TSThread], transaction: GRDBReadTransaction) throws { let pendingReadReceipts: [PendingReadReceiptRecord] = try threads.flatMap { thread -> [PendingReadReceiptRecord] in guard !thread.hasPendingMessageRequest(transaction: transaction.asAnyRead) else { return [] } return try self.finder.pendingReadReceipts(thread: thread, transaction: transaction) } let pendingViewedReceipts: [PendingViewedReceiptRecord] = try threads.flatMap { thread -> [PendingViewedReceiptRecord] in guard !thread.hasPendingMessageRequest(transaction: transaction.asAnyRead) else { return [] } return try self.finder.pendingViewedReceipts(thread: thread, transaction: transaction) } guard !pendingReadReceipts.isEmpty || !pendingViewedReceipts.isEmpty else { return } databaseStorage.asyncWrite { transaction in do { try self.enqueue(pendingReadReceipts: pendingReadReceipts, pendingViewedReceipts: pendingViewedReceipts, transaction: transaction.unwrapGrdbWrite) } catch { owsFailDebug("error: \(error)") } } } private func removeAnyReadyReceipts(threads: [TSThread], transaction: GRDBReadTransaction) throws { let pendingReadReceipts: [PendingReadReceiptRecord] = try threads.flatMap { thread -> [PendingReadReceiptRecord] in guard !thread.hasPendingMessageRequest(transaction: transaction.asAnyRead) else { return [] } return try self.finder.pendingReadReceipts(thread: thread, transaction: transaction) } let pendingViewedReceipts: [PendingViewedReceiptRecord] = try threads.flatMap { thread -> [PendingViewedReceiptRecord] in guard !thread.hasPendingMessageRequest(transaction: transaction.asAnyRead) else { return [] } return try self.finder.pendingViewedReceipts(thread: thread, transaction: transaction) } guard !pendingReadReceipts.isEmpty || !pendingViewedReceipts.isEmpty else { return } self.databaseStorage.asyncWrite { transaction in do { try self.finder.delete(pendingReadReceipts: pendingReadReceipts, transaction: transaction.unwrapGrdbWrite) try self.finder.delete(pendingViewedReceipts: pendingViewedReceipts, transaction: transaction.unwrapGrdbWrite) } catch { owsFailDebug("error: \(error)") } } } private func enqueue(pendingReadReceipts: [PendingReadReceiptRecord], pendingViewedReceipts: [PendingViewedReceiptRecord], transaction: GRDBWriteTransaction) throws { guard receiptManager.areReadReceiptsEnabled() else { Logger.info("Deleting all pending receipts - user has subsequently disabled read receipts.") try finder.deleteAllPendingReceipts(transaction: transaction) return } for receipt in pendingReadReceipts { let address = SignalServiceAddress.legacyAddress(aciString: receipt.authorAciString, phoneNumber: receipt.authorPhoneNumber) guard address.isValid else { owsFailDebug("address was invalid") continue } SSKEnvironment.shared.receiptSenderRef.enqueueReadReceipt( for: address, timestamp: UInt64(receipt.messageTimestamp), messageUniqueId: receipt.messageUniqueId, tx: transaction.asAnyWrite ) } try finder.delete(pendingReadReceipts: pendingReadReceipts, transaction: transaction) for receipt in pendingViewedReceipts { let address = SignalServiceAddress.legacyAddress(aciString: receipt.authorAciString, phoneNumber: receipt.authorPhoneNumber) guard address.isValid else { owsFailDebug("address was invalid") continue } SSKEnvironment.shared.receiptSenderRef.enqueueViewedReceipt( for: address, timestamp: UInt64(receipt.messageTimestamp), messageUniqueId: receipt.messageUniqueId, tx: transaction.asAnyWrite ) } try finder.delete(pendingViewedReceipts: pendingViewedReceipts, transaction: transaction) } } // MARK: - Persistence public class PendingReceiptFinder { public func recordPendingReadReceipt(for message: TSIncomingMessage, thread: TSThread, transaction: GRDBWriteTransaction) throws { guard let threadId = thread.sqliteRowId else { throw OWSAssertionError("threadId was unexpectedly nil") } let record = PendingReadReceiptRecord( threadId: threadId, messageTimestamp: Int64(message.timestamp), messageUniqueId: message.uniqueId, authorPhoneNumber: message.authorPhoneNumber, authorAci: Aci.parseFrom(aciString: message.authorUUID) ) try record.insert(transaction.database) } public func recordPendingViewedReceipt(for message: TSIncomingMessage, thread: TSThread, transaction: GRDBWriteTransaction) throws { guard let threadId = thread.sqliteRowId else { throw OWSAssertionError("threadId was unexpectedly nil") } let record = PendingViewedReceiptRecord( threadId: threadId, messageTimestamp: Int64(message.timestamp), messageUniqueId: message.uniqueId, authorPhoneNumber: message.authorPhoneNumber, authorAci: Aci.parseFrom(aciString: message.authorUUID) ) try record.insert(transaction.database) } public func pendingReadReceipts(thread: TSThread, transaction: GRDBReadTransaction) throws -> [PendingReadReceiptRecord] { guard let threadId = thread.sqliteRowId else { throw OWSAssertionError("threadId was unexpectedly nil") } let sql = """ SELECT * FROM pending_read_receipts WHERE threadId = \(threadId) """ return try PendingReadReceiptRecord.fetchAll(transaction.database, sql: sql) } public func pendingViewedReceipts(thread: TSThread, transaction: GRDBReadTransaction) throws -> [PendingViewedReceiptRecord] { guard let threadId = thread.sqliteRowId else { throw OWSAssertionError("threadId was unexpectedly nil") } let sql = """ SELECT * FROM pending_viewed_receipts WHERE threadId = \(threadId) """ return try PendingViewedReceiptRecord.fetchAll(transaction.database, sql: sql) } public func threadsWithPendingReceipts(transaction: GRDBReadTransaction) throws -> [TSThread] { let readSql = """ SELECT DISTINCT model_TSThread.* FROM model_TSThread INNER JOIN pending_read_receipts ON pending_read_receipts.threadId = model_TSThread.id """ let readThreads = try TSThread.grdbFetchCursor(sql: readSql, transaction: transaction).all() let viewedSql = """ SELECT DISTINCT model_TSThread.* FROM model_TSThread INNER JOIN pending_viewed_receipts ON pending_viewed_receipts.threadId = model_TSThread.id """ let viewedThreads = try TSThread.grdbFetchCursor(sql: viewedSql, transaction: transaction).all() return Array(Set(readThreads + viewedThreads)) } public func delete(pendingReadReceipts: [PendingReadReceiptRecord], transaction: GRDBWriteTransaction) throws { try PendingReadReceiptRecord.deleteAll(transaction.database, keys: pendingReadReceipts.compactMap { $0.id }) } public func delete(pendingViewedReceipts: [PendingViewedReceiptRecord], transaction: GRDBWriteTransaction) throws { try PendingViewedReceiptRecord.deleteAll(transaction.database, keys: pendingViewedReceipts.compactMap { $0.id }) } public func deleteAllPendingReceipts(transaction: GRDBWriteTransaction) throws { try PendingReadReceiptRecord.deleteAll(transaction.database) try PendingViewedReceiptRecord.deleteAll(transaction.database) } } // MARK: - fileprivate extension Notification { var userProfileWriter: UserProfileWriter { guard let userProfileWriterValue = userInfo?[kNSNotificationKey_UserProfileWriter] as? NSNumber else { owsFailDebug("userProfileWriterValue was unexpectedly nil") return .unknown } guard let userProfileWriter = UserProfileWriter(rawValue: UInt(userProfileWriterValue.intValue)) else { owsFailDebug("Invalid userProfileWriterValue") return .unknown } return userProfileWriter } func affectedThread(transaction: GRDBReadTransaction) -> TSThread? { if let address = userInfo?[UserProfileNotifications.profileAddressKey] as? SignalServiceAddress { guard let contactThread = TSContactThread.getWithContactAddress(address, transaction: transaction.asAnyRead) else { return nil } return contactThread } else { assert(userInfo?[UserProfileNotifications.profileAddressKey] == nil) } if let groupId = userInfo?[UserProfileNotifications.profileGroupIdKey] as? Data { guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction.asAnyRead) else { return nil } return groupThread } else { assert(userInfo?[UserProfileNotifications.profileGroupIdKey] == nil) } owsFailDebug("no thread details in notification") return nil } }