// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // public import LibSignalClient // MARK: - Get recipients extension TSOutgoingMessage { @objc open func recipientState(for address: SignalServiceAddress) -> TSOutgoingMessageRecipientState? { return recipientAddressStates?[address] } /// All recipients of this message. @objc public func recipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { _ in return true } } /// All recipients of this message to whom we are currently trying to send. @objc public func sendingRecipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { state in switch state.status { case .sending, .pending: return true case .skipped, .failed, .sent, .delivered, .read, .viewed: return false } } } /// All recipients of this message to whom it has been sent, including those /// for whom it has been delivered, read, or viewed. /// /// - Note: we only learn "read" status if read receipts are enabled. @objc public func sentRecipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { state in switch state.status { case .sent, .delivered, .read, .viewed: return true case .skipped, .sending, .pending, .failed: return false } } } /// All recipients of this message to whom it has been sent and delivered, /// including those for whom it has been read or viewed. /// /// - Note: we only learn "read" status if read receipts are enabled. @objc public func deliveredRecipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { state in switch state.status { case .delivered, .read, .viewed: return true case .skipped, .sending, .sent, .pending, .failed: return false } } } /// All recipients of this message to whom it has been sent, delivered, and /// read, including those for whom it has been viewed. /// /// - Note: we only learn "read" status if read receipts are enabled. @objc open func readRecipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { state in switch state.status { case .read, .viewed: return true case .skipped, .sending, .sent, .delivered, .pending, .failed: return false } } } /// All recipients of this message to whom it has been sent, delivered, and /// viewed. @objc public func viewedRecipientAddresses() -> [SignalServiceAddress] { return filterRecipientAddresses { state in switch state.status { case .viewed: return true case .skipped, .sending, .sent, .delivered, .read, .pending, .failed: return false } } } @objc public func failedRecipientAddresses(errorCode: Int) -> [SignalServiceAddress] { return filterRecipientAddresses { state in return state.status == .failed && state.errorCode == errorCode } } private func filterRecipientAddresses( predicate: (TSOutgoingMessageRecipientState) -> Bool ) -> [SignalServiceAddress] { guard let recipientAddressStates else { return [] } return recipientAddressStates.filter { _, state in predicate(state) }.map { $0.key } } } // MARK: - Update recipients public extension TSOutgoingMessage { @objc func updateWithRecipientAddressStates( _ recipientAddressStates: [SignalServiceAddress: TSOutgoingMessageRecipientState]?, tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in outgoingMessage.recipientAddressStates = recipientAddressStates } } /// Records a successful send to a one recipient. func updateWithSentRecipients( _ serviceIds: [ServiceId], wasSentByUD: Bool, transaction tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in for serviceId in serviceIds { let address = SignalServiceAddress(serviceId) guard let recipientState = outgoingMessage.recipientAddressStates?[address] else { owsFailDebug("Missing recipient state for recipient: \(address)!") continue } recipientState.updateStatusIfPossible(.sent) recipientState.wasSentByUD = wasSentByUD } } } /// Records a skipped send to multiple recipients. func updateWithSkippedRecipients( _ addresses: some Sequence, tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in for address in addresses { guard let recipientState = outgoingMessage.recipientAddressStates?[address] else { owsFailDebug("Missing recipient state for recipient: \(address)!") continue } recipientState.updateStatusIfPossible(.skipped) } } } /// Updates recipients based on information from a linked device outgoing /// message transcript. /// /// - Parameter isSentUpdate: /// If false, treats this as message creation, overwriting all existing /// recipient state. Otherwise, treats this as a sent update, only adding or /// updating recipients but never removing. func updateRecipientsFromNonLocalDevice( _ nonLocalRecipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState], isSentUpdate: Bool, transaction tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in let localRecipientStates = outgoingMessage.recipientAddressStates ?? [:] if nonLocalRecipientStates.count > 0 { /// If we have specific recipient info from the transcript, /// build new recipient states wholesale. if isSentUpdate { /// If this is a "sent update", make sure that: /// /// 1) We never remove any recipients. We end up with the /// union of the existing and new recipients. /// /// 2) We never downgrade the recipient state for any /// recipients. Prefer existing recipient state; "sent /// updates" only add new recipients in the "sent" state. var nonLocalRecipientStates = nonLocalRecipientStates for (recipientAddress, recipientState) in localRecipientStates { nonLocalRecipientStates[recipientAddress] = recipientState } outgoingMessage.recipientAddressStates = nonLocalRecipientStates } else { outgoingMessage.recipientAddressStates = nonLocalRecipientStates } } else { /// Otherwise, mark any "sending" recipients as "sent". for recipientState in localRecipientStates.values { guard recipientState.status == .sending else { continue } recipientState.updateStatusIfPossible(.sent) } } if !isSentUpdate { outgoingMessage.wasNotCreatedLocally = true } } } /// Records failed sends to the given recipients. func updateWithFailedRecipients( _ recipientErrors: some Collection<(serviceId: ServiceId, error: Error)>, tx: DBWriteTransaction ) { let fatalErrors = recipientErrors.lazy.filter { !MessageSender.isRetryableError($0.error) } let retryableErrors = recipientErrors.lazy.filter { MessageSender.isRetryableError($0.error) } if fatalErrors.isEmpty { Logger.warn("Couldn't send \(self.timestamp), but all errors are retryable: \(Array(retryableErrors))") } else { Logger.warn("Couldn't send \(self.timestamp): \(Array(fatalErrors)); retryable errors: \(Array(retryableErrors))") } self.anyUpdateOutgoingMessage(transaction: tx) { for (serviceId, error) in recipientErrors { guard let recipientState = $0.recipientAddressStates?[SignalServiceAddress(serviceId)] else { owsFailDebug("Missing recipient state for \(serviceId)") continue } if MessageSender.isRetryableError(error), recipientState.status == .sending { // For retryable errors, we can just set the error code and leave the // state set as Sending } else if error is SpamChallengeRequiredError || error is SpamChallengeResolvedError { recipientState.updateStatusIfPossible(.pending) } else { recipientState.updateStatusIfPossible(.failed) } if recipientState.canHaveErrorCode { if error is UntrustedIdentityError { recipientState.errorCode = OWSErrorCode.untrustedIdentity.rawValue } else { recipientState.errorCode = OWSErrorCode.genericFailure.rawValue } } } } } /// Mark all "sending" recipients as "failed". /// /// This should be called on app launch. @objc func updateWithAllSendingRecipientsMarkedAsFailed( error: (any Error)? = nil, transaction tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in if error is AppExpiredError { // TODO: Don't store rasterized strings in the database. outgoingMessage.mostRecentFailureText = OWSLocalizedString( "ERROR_SENDING_EXPIRED", comment: "Error indicating a send failure due to an expired application." ) } else if error is NotRegisteredError { // TODO: Don't store rasterized strings in the database. let tsAccountManager = DependenciesBridge.shared.tsAccountManager switch tsAccountManager.registrationState(tx: tx).isPrimaryDevice { case .some(true), .none: outgoingMessage.mostRecentFailureText = OWSLocalizedString( "ERROR_SENDING_DEREGISTERED", comment: "Error indicating a send failure due to a deregistered application.", ) case .some(false): outgoingMessage.mostRecentFailureText = OWSLocalizedString( "ERROR_SENDING_DELINKED", comment: "Error indicating a send failure due to a delinked application.", ) } } else if let error { outgoingMessage.mostRecentFailureText = error.userErrorDescription } guard let recipientAddressStates = outgoingMessage.recipientAddressStates else { return } for recipientState in recipientAddressStates.values { if recipientState.status == .sending { recipientState.updateStatusIfPossible(.failed) } } } } /// Mark all "failed" recipients as "sending". /// /// This should be called when we start a message send. func updateAllUnsentRecipientsAsSending( transaction tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in guard let recipientAddressStates = outgoingMessage.recipientAddressStates else { return } for recipientState in recipientAddressStates.values { if recipientState.status == .failed { recipientState.updateStatusIfPossible(.sending) } } } } /// Called when a message successfully sends. /// Subclasses that need to know when a message send succeeds can override this. @objc func updateWithSendSuccess(tx: DBWriteTransaction) { } } #if TESTABLE_BUILD public extension TSOutgoingMessage { func updateWithFakeMessageState( _ messageState: TSOutgoingMessageState, tx: DBWriteTransaction ) { anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in guard let recipientAddressStates = outgoingMessage.recipientAddressStates else { return } for recipientState in recipientAddressStates.values { switch messageState { case .sending: recipientState.updateStatusIfPossible(.sending) case .failed: recipientState.updateStatusIfPossible(.failed) case .sent: recipientState.updateStatusIfPossible(.sent) case .pending: recipientState.updateStatusIfPossible(.pending) case .sent_OBSOLETE, .delivered_OBSOLETE: break } } } } } #endif // MARK: - public extension TSOutgoingMessage { @objc static func messageStateForRecipientStates( _ recipientStates: [TSOutgoingMessageRecipientState] ) -> TSOutgoingMessageState { var hasSendingReceipient = false var hasPendingRecipient = false var hasFailedRecipient = false for recipientState in recipientStates { switch recipientState.status { case .sending: hasSendingReceipient = true case .pending: hasPendingRecipient = true case .failed: hasFailedRecipient = true case .skipped, .sent, .delivered, .read, .viewed: break } } if hasSendingReceipient { return .sending } if hasPendingRecipient { return .pending } if hasFailedRecipient { return .failed } return .sent } @objc static func isEligibleToStartExpireTimer(recipientStates: [TSOutgoingMessageRecipientState]) -> Bool { let messageState = Self.messageStateForRecipientStates(recipientStates) return isEligibleToStartExpireTimer(messageState: messageState) } // This method will be called after every insert and update, so it needs // to be cheap. @objc static func isEligibleToStartExpireTimer(messageState: TSOutgoingMessageState) -> Bool { switch messageState { case .sent, .sent_OBSOLETE, .delivered_OBSOLETE: // If _all_ recipients have been sent (not necessarily received or viewed) // we should start the expire timer. return true case .pending, .sending, .failed: // If _any_ recipient is pending or failed, don't start the timer. return false } } @objc var isStorySend: Bool { isGroupStoryReply } @objc(buildPniSignatureMessageIfNeededWithTransaction:) func buildPniSignatureMessageIfNeeded(transaction tx: DBReadTransaction) -> SSKProtoPniSignatureMessage? { guard recipientAddressStates?.count == 1 else { // This is probably a group message, nothing to be alarmed about. return nil } guard let recipientServiceId = recipientAddressStates!.keys.first!.serviceId else { return nil } let identityManager = DependenciesBridge.shared.identityManager guard identityManager.shouldSharePhoneNumber(with: recipientServiceId, tx: tx) else { // No PNI signature needed. return nil } guard let pni = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx)?.pni else { owsFailDebug("missing PNI") return nil } guard let pniIdentityKeyPair = identityManager.identityKeyPair(for: .pni, tx: tx) else { owsFailDebug("missing PNI identity key") return nil } guard let aciIdentityKeyPair = identityManager.identityKeyPair(for: .aci, tx: tx) else { owsFailDebug("missing ACI identity key") return nil } let signature = pniIdentityKeyPair.identityKeyPair.signAlternateIdentity( aciIdentityKeyPair.identityKeyPair.identityKey) let builder = SSKProtoPniSignatureMessage.builder() builder.setPni(pni.rawUUID.data) builder.setSignature(signature) return builder.buildInfallibly() } @objc func addGroupsV2ToDataMessageBuilder( _ builder: SSKProtoDataMessageBuilder, groupThread: TSGroupThread, tx: DBReadTransaction ) -> OutgoingGroupProtoResult { guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { owsFailDebug("Invalid group model.") return .error } do { let groupContextV2 = try GroupsV2Protos.buildGroupContextProto( groupModel: groupModel, groupChangeProtoData: self.changeActionsProtoData ) builder.setGroupV2(groupContextV2) return .addedWithoutGroupAvatar } catch { owsFailDebug("Error: \(error)") return .error } } fileprivate func maybeClearShouldSharePhoneNumber( for recipientAddress: SignalServiceAddress, recipientDeviceId deviceId: DeviceId, transaction: DBWriteTransaction ) { guard let aci = recipientAddress.serviceId as? Aci else { // We can't be sharing our phone number b/c there's no ACI. return } guard recipientAddressStates?[recipientAddress]?.wasSentByUD == true else { // Can't be sure the message was actually decrypted by the recipient, // because the server sends delivery receipts for non-sealed-sender messages. return } let identityManager = DependenciesBridge.shared.identityManager guard identityManager.shouldSharePhoneNumber(with: aci, tx: transaction) else { // Not currently sharing anyway! return } let messageSendLog = SSKEnvironment.shared.messageSendLogRef let messagePayload = messageSendLog.fetchPayload( recipientAci: aci, recipientDeviceId: deviceId, timestamp: timestamp, tx: transaction ) guard let messagePayload, let payloadId = messagePayload.payloadId else { // Can't check whether this message included a PNI signature. return } let deviceIdsPendingDelivery = messageSendLog.deviceIdsPendingDelivery( for: payloadId, recipientAci: aci, tx: transaction ) guard let deviceIdsPendingDelivery, deviceIdsPendingDelivery == [deviceId] else { // Other devices still need the PniSignature. return } guard let content = try? SSKProtoContent(serializedData: messagePayload.plaintextContent), let messagePniData = content.pniSignatureMessage?.pni else { // No PNI signature in the message. return } guard let currentPni = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction)?.pni else { owsFailDebug("missing local PNI") return } if messagePniData == currentPni.rawUUID.data { identityManager.clearShouldSharePhoneNumber(with: aci, tx: transaction) } } } // MARK: - Attachments extension TSOutgoingMessage { @objc func buildProtosForBodyAttachments(tx: DBReadTransaction) throws -> [SSKProtoAttachmentPointer] { let attachments = sqliteRowId.map { sqliteRowId in return DependenciesBridge.shared.attachmentStore.fetchReferencedAttachments( owners: [ .messageOversizeText(messageRowId: sqliteRowId), .messageBodyAttachment(messageRowId: sqliteRowId) ], tx: tx ) } ?? [] return attachments.compactMap { attachment in guard let pointer = attachment.attachment.asTransitTierPointer(), case let .digestSHA256Ciphertext(digestSHA256Ciphertext) = pointer.info.integrityCheck else { owsFailDebug("Generating proto for non-uploaded attachment!") return nil } return DependenciesBridge.shared.attachmentManager.buildProtoForSending( from: attachment.reference, pointer: pointer, digestSHA256Ciphertext: digestSHA256Ciphertext ) } } @objc func buildLinkPreviewProto( linkPreview: OWSLinkPreview, tx: DBReadTransaction ) throws -> SSKProtoPreview { return try DependenciesBridge.shared.linkPreviewManager.buildProtoForSending( linkPreview, parentMessage: self, tx: tx ) } @objc func buildContactShareProto( _ contact: OWSContact, tx: DBReadTransaction ) throws -> SSKProtoDataMessageContact { return try DependenciesBridge.shared.contactShareManager.buildProtoForSending( from: contact, parentMessage: self, tx: tx ) } @objc func buildStickerProto( sticker: MessageSticker, tx: DBReadTransaction ) throws -> SSKProtoDataMessageSticker { return try DependenciesBridge.shared.messageStickerManager.buildProtoForSending( sticker, parentMessage: self, tx: tx ) } @objc func buildQuoteProto( quote: TSQuotedMessage, tx: DBReadTransaction ) throws -> SSKProtoDataMessageQuote { return try DependenciesBridge.shared.quotedReplyManager.buildProtoForSending( quote, parentMessage: self, tx: tx ) } // MARK: - Polls @objc func buildPollProto(tx: DBReadTransaction) -> SSKProtoDataMessagePollCreate? { do { return try DependenciesBridge.shared.pollMessageManager.buildProtoForSending( parentMessage: self, tx: tx ) } catch { return nil } } @objc func shouldBumpProtoForPolls() -> Bool { return !FeatureFlags.pollKeepProtoVersion } } // MARK: - Receipts extension TSOutgoingMessage { public func update( withDeliveredRecipient recipientAddress: SignalServiceAddress, deviceId: DeviceId, deliveryTimestamp timestamp: UInt64, context: DeliveryReceiptContext, tx: DBWriteTransaction ) { handleReceipt( from: recipientAddress, deviceId: deviceId, receiptType: .delivered, receiptTimestamp: timestamp, tryToClearPhoneNumberSharing: true, tx: tx ) } public func update( withReadRecipient recipientAddress: SignalServiceAddress, deviceId: DeviceId, readTimestamp timestamp: UInt64, tx: DBWriteTransaction ) { handleReceipt( from: recipientAddress, deviceId: deviceId, receiptType: .read, receiptTimestamp: timestamp, tx: tx ) } public func update( withViewedRecipient recipientAddress: SignalServiceAddress, deviceId: DeviceId, viewedTimestamp timestamp: UInt64, tx: DBWriteTransaction ) { handleReceipt( from: recipientAddress, deviceId: deviceId, receiptType: .viewed, receiptTimestamp: timestamp, tx: tx ) } private enum IncomingReceiptType { case delivered case read case viewed var asRecipientStatus: OWSOutgoingMessageRecipientStatus { switch self { case .delivered: return .delivered case .read: return .read case .viewed: return .viewed } } } private func handleReceipt( from recipientAddress: SignalServiceAddress, deviceId: DeviceId, receiptType: IncomingReceiptType, receiptTimestamp: UInt64, tryToClearPhoneNumberSharing: Bool = false, tx: DBWriteTransaction ) { owsAssertDebug(recipientAddress.isValid) // Ignore receipts for messages that have been deleted. They are no longer // relevant to this message. if wasRemotelyDeleted { return } // Note that this relies on the Message Send Log, so we have to execute it first. if tryToClearPhoneNumberSharing { maybeClearShouldSharePhoneNumber(for: recipientAddress, recipientDeviceId: deviceId, transaction: tx) } // This is only necessary for delivery receipts, but while we're here with // an open write transaction, we check it for other receipts as well. clearMessageSendLogEntry(forRecipient: recipientAddress, deviceId: deviceId, tx: tx) let recipientStateMerger = RecipientStateMerger( recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable, signalServiceAddressCache: SSKEnvironment.shared.signalServiceAddressCacheRef ) anyUpdateOutgoingMessage(transaction: tx) { message in guard let recipientState: TSOutgoingMessageRecipientState = { if let existingMatch = message.recipientAddressStates?[recipientAddress] { return existingMatch } if let normalizedAddress = recipientStateMerger.normalizedAddressIfNeeded(for: recipientAddress, tx: tx) { // If we get a receipt from a PNI, then normalizing PNIs -> ACIs won't fix // it, but normalizing the address from a PNI to an ACI might fix it. return message.recipientAddressStates?[normalizedAddress] } else { // If we get a receipt from an ACI, then we might have the PNI stored, and // we need to migrate it to the ACI before we'll be able to find it. recipientStateMerger.normalize(&message.recipientAddressStates, tx: tx) return message.recipientAddressStates?[recipientAddress] } }() else { owsFailDebug("Missing recipient state for \(recipientAddress)") return } recipientState.updateStatusIfPossible( receiptType.asRecipientStatus, statusTimestamp: receiptTimestamp ) } } } // MARK: - Sender Key + Message Send Log extension TSOutgoingMessage { /// A collection of message unique IDs related to the outgoing message /// /// Used to help prune the Message Send Log. For example, a properly annotated outgoing reaction /// message will automatically be deleted from the Message Send Log when the reacted message is /// deleted. /// /// Subclasses should override to include any interactionIds their specific subclass relates to. Subclasses /// *probably* want to return a union with the results of their parent class' implementation @objc var relatedUniqueIds: Set { Set([self.uniqueId]) } /// Returns a content hint appropriate for representing this content /// /// If a message is sent with sealed sender, this will be included inside the envelope. A recipient who's /// able to decrypt the envelope, but unable to decrypt the inner content can use this to infer how to /// handle recovery based on the user-visibility of the content and likelihood of recovery. /// /// See: SealedSenderContentHint @objc var contentHint: SealedSenderContentHint { .resendable } /// Returns a groupId relevant to the message. This is included in the envelope, outside the content encryption. /// /// Usually, this will be the groupId of the target thread. However, there's a special case here where message resend /// responses will inherit the groupId of the original message. This probably shouldn't be overridden by anything except /// OWSOutgoingMessageResendResponse @objc func envelopeGroupIdWithTransaction(_ transaction: DBReadTransaction) -> Data? { (thread(tx: transaction) as? TSGroupThread)?.groupId } /// Indicates whether or not this message's proto should be saved into the MessageSendLog /// /// Anything high volume or time-dependent (typing indicators, calls, etc.) should set this false. /// A non-resendable content hint does not necessarily mean this should be false set false (though /// it is a good indicator) @objc var shouldRecordSendLog: Bool { true } /// Used in MessageSender to signal how a message should be encrypted before sending /// Currently only overridden by OWSOutgoingResendRequest (this is asserted in the MessageSender implementation) @objc var encryptionStyle: EncryptionStyle { .whisper } func clearMessageSendLogEntry(forRecipient address: SignalServiceAddress, deviceId: DeviceId, tx: DBWriteTransaction) { // MSL entries will only exist for addresses with ACIs guard let aci = address.serviceId as? Aci else { return } let messageSendLog = SSKEnvironment.shared.messageSendLogRef messageSendLog.recordSuccessfulDelivery( message: self, recipientAci: aci, recipientDeviceId: deviceId, tx: tx ) } @objc func markMessageSendLogEntryCompleteIfNeeded(tx: DBWriteTransaction) { guard sendingRecipientAddresses().isEmpty else { return } let messageSendLog = SSKEnvironment.shared.messageSendLogRef messageSendLog.sendComplete(message: self, tx: tx) } } // MARK: - Transcripts public extension TSOutgoingMessage { func sendSyncTranscript() async throws { let messageSend = try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in guard let localThread = TSContactThread.getOrCreateLocalThread(transaction: tx) else { throw OWSAssertionError("Missing local thread") } guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx) else { throw OWSAssertionError("Missing localIdentifiers.") } guard let transcript = self.buildTranscriptSyncMessage(localThread: localThread, transaction: tx) else { throw OWSAssertionError("Failed to build transcript") } guard let serializedMessage = SSKEnvironment.shared.messageSenderRef.buildAndRecordMessage(transcript, in: localThread, tx: tx) else { throw OWSAssertionError("Couldn't serialize message.") } return OWSMessageSend( message: transcript, plaintextContent: serializedMessage.plaintextData, plaintextPayloadId: serializedMessage.payloadId, thread: localThread, serviceId: localIdentifiers.aci, localIdentifiers: localIdentifiers ) } try await SSKEnvironment.shared.messageSenderRef.performMessageSend(messageSend, sealedSenderParameters: nil) } }