Signal-iOS/SignalServiceKit/Messages/Interactions/TSOutgoingMessage.swift
2026-05-22 21:10:33 -05:00

922 lines
35 KiB
Swift

//
// 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 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: some Sequence<ServiceId>,
wasSentByUD: Bool,
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<SignalServiceAddress>,
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 Sequence<(serviceId: ServiceId, error: Error)>,
tx: DBWriteTransaction,
) {
let fatalErrors = recipientErrors.filter { !MessageSender.isRetryableError($0.error) }
let retryableErrors = recipientErrors.filter { MessageSender.isRetryableError($0.error) }
if fatalErrors.isEmpty {
Logger.warn("Couldn't send \(self.timestamp), but all errors are retryable: \(retryableErrors)")
} else {
Logger.warn("Couldn't send \(self.timestamp): \(fatalErrors); retryable errors: \(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 {
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
public 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
public func updateWithSendSuccess(tx: DBWriteTransaction) { }
// MARK: -
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
public var isStorySend: Bool { isGroupStoryReply }
@objc
func _buildPlaintextData(inThread thread: TSThread, tx: DBWriteTransaction) throws -> Data {
guard let contentBuilder = self.contentBuilder(thread: thread, transaction: tx) else {
throw OWSAssertionError("couldn't build protobuf")
}
if let pniSignatureMessage = self.buildPniSignatureMessageIfNeeded(tx: tx) {
contentBuilder.setPniSignatureMessage(pniSignatureMessage)
}
return try contentBuilder.buildSerializedData()
}
@objc
func _dataMessageBuilder(thread: TSThread, tx: DBReadTransaction) -> SSKProtoDataMessageBuilder? {
let builder = SSKProtoDataMessage.builder()
builder.setTimestamp(self.timestamp)
var requiredProtocolVersion = SSKProtoDataMessageProtocolVersion.initial.rawValue
if self.isViewOnceMessage {
builder.setIsViewOnce(true)
requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.viewOnceVideo.rawValue)
}
let trimmedBody = self.body?.trimToUtf8ByteCount(OWSMediaUtils.kOversizeTextMessageSizeThresholdBytes)
// It was historically possible to end up with a message in the database that
// exceeds this threshold, and therefore possible to hit this assert (by forwarding
// an older message). But it is good for us to know when this happens.
owsAssertDebug(self.body?.utf8.count == trimmedBody?.utf8.count)
if self.isPoll {
if let pollCreateProto = self.buildPollProto(tx: tx) {
builder.setPollCreate(pollCreateProto)
} else {
owsFailDebug("Could not build poll protobuf")
}
requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.polls.rawValue)
} else {
if let trimmedBody {
builder.setBody(trimmedBody)
let bodyRanges = self.bodyRanges?.toProtoBodyRanges(bodyLength: trimmedBody.utf16.count) ?? []
if !bodyRanges.isEmpty {
builder.setBodyRanges(bodyRanges)
requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.mentions.rawValue)
}
}
}
// Story Context
if let storyTimestamp, let storyAuthorAci {
if let storyReactionEmoji {
let reactionBuilder = SSKProtoDataMessageReaction.builder(emoji: storyReactionEmoji, timestamp: storyTimestamp.uint64Value)
reactionBuilder.setTargetAuthorAciBinary(storyAuthorAci.serviceIdBinary)
do {
builder.setReaction(try reactionBuilder.build())
requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.reactions.rawValue)
} catch {
owsFailDebug("Could not build story reaction protobuf: \(error)")
}
}
let storyContextBuilder = SSKProtoDataMessageStoryContext.builder()
storyContextBuilder.setAuthorAciBinary(storyAuthorAci.serviceIdBinary)
storyContextBuilder.setSentTimestamp(storyTimestamp.uint64Value)
builder.setStoryContext(storyContextBuilder.buildInfallibly())
}
builder.setExpireTimer(self.expiresInSeconds)
if let expireTimerVersion {
owsAssertDebug(expireTimerVersion.uint32Value >= 1)
builder.setExpireTimerVersion(expireTimerVersion.uint32Value)
}
// Group Messages
if let thread = thread as? TSGroupThread {
switch thread.groupModel.groupsVersion {
case .V1:
Logger.error("[GV1] Cannot build data message for V1 group!")
return nil
case .V2:
break
}
let result = self.addGroupsV2ToDataMessageBuilder(builder, groupThread: thread, tx: tx)
switch result {
case .error:
return nil
case .addedWithoutGroupAvatar:
break
}
}
// Message Attachments
// Only inserted messages should have attachments, and if they are saveable
// they should be inserted by now.
if self.shouldBeSaved {
if grdbId != nil {
let attachments = buildProtosForBodyAttachments(tx: tx)
builder.setAttachments(attachments)
} else {
owsFailDebug("Saved message uninserted at proto build time!")
}
}
// Quoted Reply
if let quotedMessage {
do {
let quoteProto = try self.buildQuoteProto(quote: quotedMessage, tx: tx)
builder.setQuote(quoteProto)
if !quoteProto.bodyRanges.isEmpty {
requiredProtocolVersion = max(requiredProtocolVersion, SSKProtoDataMessageProtocolVersion.mentions.rawValue)
}
} catch {
owsFailDebug("Could not build quote protobuf: \(error)")
}
}
// Contact Share
if let contactShare {
do {
let contactProto = try self.buildContactShareProto(contactShare, tx: tx)
builder.addContact(contactProto)
} catch {
owsFailDebug("Could not build contact share protobuf: \(error)")
}
}
// Link Preview
if let linkPreview {
do {
let previewProto = try self.buildLinkPreviewProto(linkPreview: linkPreview, tx: tx)
builder.addPreview(previewProto)
} catch {
owsFailDebug("Could not build link preview protobuf: \(error)")
}
}
// Sticker
if let messageSticker {
do {
let stickerProto = try self.buildStickerProto(sticker: messageSticker, tx: tx)
builder.setSticker(stickerProto)
} catch {
owsFailDebug("Could not build sticker protobuf: \(error)")
}
}
// Gift badge
if let giftBadge {
let giftBadgeBuilder = SSKProtoDataMessageGiftBadge.builder()
if let redemptionCredential = giftBadge.redemptionCredential {
giftBadgeBuilder.setReceiptCredentialPresentation(redemptionCredential)
}
builder.setGiftBadge(giftBadgeBuilder.buildInfallibly())
}
builder.setRequiredProtocolVersion(UInt32(requiredProtocolVersion))
return builder
}
@objc
func _buildSyncTranscriptMessage(localThread: TSContactThread, tx: DBWriteTransaction) throws -> OutgoingSyncMessage {
owsAssertDebug(self.shouldSyncTranscript())
guard let messageThread = self.thread(tx: tx) else {
throw OWSAssertionError("missing thread for sent message")
}
return OutgoingSentMessageTranscript(
localThread: localThread,
messageThread: messageThread,
message: self,
isRecipientUpdate: self.hasSyncedTranscript,
tx: tx,
)
}
private func buildPniSignatureMessageIfNeeded(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()
}
private 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
}
}
private 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
private func buildProtosForBodyAttachments(tx: DBReadTransaction) -> [SSKProtoAttachmentPointer] {
let referencedAttachments = sqliteRowId.map { sqliteRowId in
return DependenciesBridge.shared.attachmentStore.fetchReferencedAttachments(
owners: [
.messageOversizeText(messageRowId: sqliteRowId),
.messageBodyAttachment(messageRowId: sqliteRowId),
],
tx: tx,
)
} ?? []
return referencedAttachments.compactMap { referencedAttachment in
guard let attachmentProto = referencedAttachment.asProtoForSending() else {
owsFailDebug("Failed to generate proto for body attachment!")
return nil
}
return attachmentProto
}
}
private func buildLinkPreviewProto(
linkPreview: OWSLinkPreview,
tx: DBReadTransaction,
) throws -> SSKProtoPreview {
return try DependenciesBridge.shared.linkPreviewManager.buildProtoForSending(
linkPreview,
parentMessage: self,
tx: tx,
)
}
private func buildContactShareProto(
_ contact: OWSContact,
tx: DBReadTransaction,
) throws -> SSKProtoDataMessageContact {
return try DependenciesBridge.shared.contactShareManager.buildProtoForSending(
from: contact,
parentMessage: self,
tx: tx,
)
}
private func buildStickerProto(
sticker: MessageSticker,
tx: DBReadTransaction,
) throws -> SSKProtoDataMessageSticker {
return try DependenciesBridge.shared.messageStickerManager.buildProtoForSending(
sticker,
parentMessage: self,
tx: tx,
)
}
private func buildQuoteProto(
quote: TSQuotedMessage,
tx: DBReadTransaction,
) throws -> SSKProtoDataMessageQuote {
return try DependenciesBridge.shared.quotedReplyManager.buildProtoForSending(
quote,
outgoingMessage: self,
tx: tx,
)
}
// MARK: - Polls
private func buildPollProto(tx: DBReadTransaction) -> SSKProtoDataMessagePollCreate? {
do {
return try DependenciesBridge.shared.pollMessageManager.buildProtoForSending(
parentMessage: self,
tx: tx,
)
} catch {
return nil
}
}
// MARK: - Receipts
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,
context: context,
tx: tx,
)
}
public func update(
withReadRecipient recipientAddress: SignalServiceAddress,
deviceId: DeviceId,
readTimestamp timestamp: UInt64,
tx: DBWriteTransaction,
) {
handleReceipt(
from: recipientAddress,
deviceId: deviceId,
receiptType: .read,
receiptTimestamp: timestamp,
context: PassthroughDeliveryReceiptContext(),
tx: tx,
)
}
public func update(
withViewedRecipient recipientAddress: SignalServiceAddress,
deviceId: DeviceId,
viewedTimestamp timestamp: UInt64,
tx: DBWriteTransaction,
) {
handleReceipt(
from: recipientAddress,
deviceId: deviceId,
receiptType: .viewed,
receiptTimestamp: timestamp,
context: PassthroughDeliveryReceiptContext(),
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,
context: any DeliveryReceiptContext,
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,
)
context.addUpdate(
message: self,
transaction: tx,
update: { 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
/// 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<String> {
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 OutgoingResendRequest (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)
}
}