TM-SGNL-iOS/SignalServiceKit/Messages/Interactions/TSOutgoingMessage.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

828 lines
30 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 extension TSOutgoingMessage {
@objc
func updateWithRecipientAddressStates(
_ recipientAddressStates: [SignalServiceAddress: TSOutgoingMessageRecipientState]?,
tx: SDSAnyWriteTransaction
) {
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
outgoingMessage.recipientAddressStates = recipientAddressStates
}
}
/// Records a successful send to a one recipient.
func updateWithSentRecipient(
_ serviceId: ServiceId,
wasSentByUD: Bool,
transaction tx: SDSAnyWriteTransaction
) {
let address = SignalServiceAddress(serviceId)
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
guard let recipientState = outgoingMessage.recipientAddressStates?[address] else {
owsFailDebug("Missing recipient state for recipient: \(address)!")
return
}
recipientState.updateStatus(.sent)
recipientState.wasSentByUD = wasSentByUD
recipientState.errorCode = nil
}
}
/// Records a skipped send to one recipient.
func updateWithSkippedRecipient(
_ address: SignalServiceAddress,
transaction tx: SDSAnyWriteTransaction
) {
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
guard let recipientState = outgoingMessage.recipientAddressStates?[address] else {
owsFailDebug("Missing recipient state for recipient: \(address)!")
return
}
recipientState.updateStatus(.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: SDSAnyWriteTransaction
) {
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.updateStatus(.sent)
}
}
if !isSentUpdate {
outgoingMessage.wasNotCreatedLocally = true
}
}
}
/// Records failed sends to the given recipients.
func updateWithFailedRecipients(
_ recipientErrors: some Collection<(serviceId: ServiceId, error: Error)>,
tx: SDSAnyWriteTransaction
) {
let fatalErrors = recipientErrors.lazy.filter { !$0.error.isRetryable }
let retryableErrors = recipientErrors.lazy.filter { $0.error.isRetryable }
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 error.isRetryable, 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.updateStatus(.pending)
} else {
recipientState.updateStatus(.failed)
}
recipientState.errorCode = (error as NSError).code
}
}
}
/// Mark all "sending" recipients as "failed".
///
/// This should be called on app launch.
@objc
func updateWithAllSendingRecipientsMarkedAsFailed(
error: (any Error)? = nil,
transaction tx: SDSAnyWriteTransaction
) {
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
if let error {
outgoingMessage.mostRecentFailureText = error.userErrorDescription
}
guard let recipientAddressStates = outgoingMessage.recipientAddressStates else {
return
}
for recipientState in recipientAddressStates.values {
if recipientState.status == .sending {
recipientState.updateStatus(.failed)
}
}
}
}
/// Mark all "failed" recipients as "sending".
///
/// This should be called when we start a message send.
func updateAllUnsentRecipientsAsSending(
transaction tx: SDSAnyWriteTransaction
) {
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
guard let recipientAddressStates = outgoingMessage.recipientAddressStates else {
return
}
for recipientState in recipientAddressStates.values {
if recipientState.status == .failed {
recipientState.updateStatus(.sending)
}
}
}
}
}
#if TESTABLE_BUILD
public extension TSOutgoingMessage {
func updateWithFakeMessageState(
_ messageState: TSOutgoingMessageState,
tx: SDSAnyWriteTransaction
) {
anyUpdateOutgoingMessage(transaction: tx) { outgoingMessage in
guard let recipientAddressStates = outgoingMessage.recipientAddressStates else {
return
}
for recipientState in recipientAddressStates.values {
switch messageState {
case .sending:
recipientState.updateStatus(.sending)
case .failed:
recipientState.updateStatus(.failed)
case .sent:
recipientState.updateStatus(.sent)
case .pending:
recipientState.updateStatus(.pending)
case .sent_OBSOLETE, .delivered_OBSOLETE:
break
}
}
}
}
}
#endif
// MARK: -
public extension TSOutgoingMessage {
@objc
static func messageStateForRecipientStates(
_ recipientStates: [TSOutgoingMessageRecipientState]
) -> TSOutgoingMessageState {
var hasFailedRecipient: Bool = false
for recipientState in recipientStates {
switch recipientState.status {
case .sending:
return .sending
case .pending:
return .pending
case .failed:
hasFailedRecipient = true
case .skipped, .sent, .delivered, .read, .viewed:
break
}
}
if hasFailedRecipient {
return .failed
} else {
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: SDSAnyReadTransaction) -> 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.asV2Read) else {
// No PNI signature needed.
return nil
}
guard let pni = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.pni else {
owsFailDebug("missing PNI")
return nil
}
guard let pniIdentityKeyPair = identityManager.identityKeyPair(for: .pni, tx: tx.asV2Read) else {
owsFailDebug("missing PNI identity key")
return nil
}
guard let aciIdentityKeyPair = identityManager.identityKeyPair(for: .aci, tx: tx.asV2Read) 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(Data(signature))
return builder.buildInfallibly()
}
@objc
func addGroupsV2ToDataMessageBuilder(
_ builder: SSKProtoDataMessageBuilder,
groupThread: TSGroupThread,
tx: SDSAnyReadTransaction
) -> 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: UInt32,
transaction: SDSAnyWriteTransaction
) {
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.asV2Read) 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.asV2Read)?.pni else {
owsFailDebug("missing local PNI")
return
}
if messagePniData == currentPni.rawUUID.data {
identityManager.clearShouldSharePhoneNumber(with: aci, tx: transaction.asV2Write)
}
}
}
// MARK: - Attachments
extension TSOutgoingMessage {
@objc
func buildProtosForBodyAttachments(tx: SDSAnyReadTransaction) throws -> [SSKProtoAttachmentPointer] {
let attachments = sqliteRowId.map { sqliteRowId in
return DependenciesBridge.shared.attachmentStore.fetchReferencedAttachments(
owners: [
.messageOversizeText(messageRowId: sqliteRowId),
.messageBodyAttachment(messageRowId: sqliteRowId)
],
tx: tx.asV2Read
)
} ?? []
return attachments.compactMap { attachment in
guard let pointer = attachment.attachment.asTransitTierPointer() else {
owsFailDebug("Generating proto for non-uploaded attachment!")
return nil
}
return DependenciesBridge.shared.attachmentManager.buildProtoForSending(
from: attachment.reference,
pointer: pointer
)
}
}
@objc
func buildLinkPreviewProto(
linkPreview: OWSLinkPreview,
tx: SDSAnyReadTransaction
) throws -> SSKProtoPreview {
return try DependenciesBridge.shared.linkPreviewManager.buildProtoForSending(
linkPreview,
parentMessage: self,
tx: tx.asV2Read
)
}
@objc
func buildContactShareProto(
_ contact: OWSContact,
tx: SDSAnyReadTransaction
) throws -> SSKProtoDataMessageContact {
return try DependenciesBridge.shared.contactShareManager.buildProtoForSending(
from: contact,
parentMessage: self,
tx: tx.asV2Read
)
}
@objc
func buildStickerProto(
sticker: MessageSticker,
tx: SDSAnyReadTransaction
) throws -> SSKProtoDataMessageSticker {
return try DependenciesBridge.shared.messageStickerManager.buildProtoForSending(
sticker,
parentMessage: self,
tx: tx.asV2Read
)
}
@objc
func buildQuoteProto(
quote: TSQuotedMessage,
tx: SDSAnyReadTransaction
) throws -> SSKProtoDataMessageQuote {
return try DependenciesBridge.shared.quotedReplyManager.buildProtoForSending(
quote,
parentMessage: self,
tx: tx.asV2Read
)
}
}
// MARK: - Receipts
extension TSOutgoingMessage {
public func update(
withDeliveredRecipient recipientAddress: SignalServiceAddress,
deviceId: UInt32,
deliveryTimestamp timestamp: UInt64,
context: DeliveryReceiptContext,
tx: SDSAnyWriteTransaction
) {
handleReceipt(
from: recipientAddress,
deviceId: deviceId,
receiptType: .delivered,
receiptTimestamp: timestamp,
tryToClearPhoneNumberSharing: true,
tx: tx
)
}
public func update(
withReadRecipient recipientAddress: SignalServiceAddress,
deviceId: UInt32,
readTimestamp timestamp: UInt64,
tx: SDSAnyWriteTransaction
) {
handleReceipt(
from: recipientAddress,
deviceId: deviceId,
receiptType: .read,
receiptTimestamp: timestamp,
tx: tx
)
}
public func update(
withViewedRecipient recipientAddress: SignalServiceAddress,
deviceId: UInt32,
viewedTimestamp timestamp: UInt64,
tx: SDSAnyWriteTransaction
) {
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: UInt32,
receiptType: IncomingReceiptType,
receiptTimestamp: UInt64,
tryToClearPhoneNumberSharing: Bool = false,
tx: SDSAnyWriteTransaction
) {
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.asV2Read) {
// 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.asV2Read)
return message.recipientAddressStates?[recipientAddress]
}
}() else {
owsFailDebug("Missing recipient state for \(recipientAddress)")
return
}
/// We want to avoid "downgrading" the recipient status; for
/// example, if we receive a delivery receipt after a read receipt,
/// we want to preserve the `.read` status.
///
/// We do, however, support overwriting the recipient status'
/// timestamp when receiving a receipt matching the existing
/// recipient status (e.g., receiving a `.read` receipt when the
/// recipient status is already `.read`).
let shouldUpdateRecipientStatus: Bool = {
switch (recipientState.status, receiptType) {
case (.failed, _), (.sending, _), (.sent, _), (.skipped, _), (.pending, _):
return true
case
(.delivered, .delivered),
(.delivered, .read),
(.delivered, .viewed),
(.read, .read),
(.read, .viewed),
(.viewed, .viewed):
return true
case
(.read, .delivered),
(.viewed, .delivered),
(.viewed, .read):
return false
}
}()
if shouldUpdateRecipientStatus {
recipientState.updateStatus(
receiptType.asRecipientStatus,
statusTimestamp: receiptTimestamp
)
recipientState.errorCode = nil
}
}
}
}
// 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<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: SDSAnyReadTransaction) -> 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 }
@objc
func clearMessageSendLogEntry(forRecipient address: SignalServiceAddress, deviceId: UInt32, tx: SDSAnyWriteTransaction) {
// 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: SDSAnyWriteTransaction) {
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.asV2Read) 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)
}
}