922 lines
35 KiB
Swift
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)
|
|
}
|
|
}
|