// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient public class OWSIncomingSentMessageTranscript: SentMessageTranscript { public let type: SentMessageTranscriptType public var requiredProtocolVersion: UInt32? public let timestamp: UInt64 public let recipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState] public static func from( sentProto: SSKProtoSyncMessageSent, serverTimestamp: UInt64, tx: DBWriteTransaction, ) -> OWSIncomingSentMessageTranscript? { let isEdit = sentProto.editMessage?.dataMessage != nil guard let dataMessage = sentProto.message ?? sentProto.editMessage?.dataMessage else { owsFailDebug("Missing message.") return nil } guard sentProto.timestamp != 0 else { owsFailDebug("Sent missing timestamp.") return nil } let recipientAddress: SignalServiceAddress? let groupId: GroupIdentifier? if let groupContextV2 = dataMessage.groupV2 { guard let masterKey = groupContextV2.masterKey else { owsFailDebug("Missing masterKey.") return nil } guard let contextInfo = try? GroupV2ContextInfo.deriveFrom(masterKeyData: masterKey) else { owsFailDebug("Couldn't parse contextInfo.") return nil } groupId = contextInfo.groupId recipientAddress = nil } else if sentProto.hasDestinationServiceID || sentProto.hasDestinationServiceIDBinary || sentProto.destinationE164 != nil { let serviceId = ServiceId.parseFrom( serviceIdBinary: sentProto.destinationServiceIDBinary, serviceIdString: sentProto.destinationServiceID, ) let destinationAddress = SignalServiceAddress( serviceId: serviceId, legacyPhoneNumber: sentProto.destinationE164?.nilIfEmpty, cache: SSKEnvironment.shared.signalServiceAddressCacheRef, ) guard destinationAddress.isValid else { owsFailDebug("Invalid destinationAddress.") return nil } groupId = nil recipientAddress = destinationAddress } else { owsFailDebug("Neither a group ID nor recipient address found!") return nil } var isExpirationTimerUpdate = false if dataMessage.hasFlags { let flags = Int32(dataMessage.flags) isExpirationTimerUpdate = (flags & SSKProtoDataMessageFlags.expirationTimerUpdate.rawValue) != 0 } let type: SentMessageTranscriptType if sentProto.isRecipientUpdate, !isEdit { guard let groupId, let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { owsFailDebug("We should never receive a 'recipient update' for messages in contact threads.") return nil } type = .recipientUpdate(groupThread) } else if isExpirationTimerUpdate { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx, ) else { return nil } type = .expirationTimerUpdate(target) } else if dataMessage.payment != nil { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx, ) else { return nil } guard let paymentModels = TSPaymentModels.parsePaymentProtos(dataMessage: dataMessage, thread: target.thread) else { return nil } let paymentServerTimestamp: UInt64 if serverTimestamp > 0 { paymentServerTimestamp = serverTimestamp } else { // We fall back to the sent timestamp, even though this is called a server timestamp. // This was done historically and behavior is maintained. paymentServerTimestamp = sentProto.timestamp } owsAssertDebug(paymentServerTimestamp > 0) let paymentNotification = SentMessageTranscriptType.PaymentNotification( target: target, serverTimestamp: paymentServerTimestamp, notification: paymentModels.notification, ) type = .paymentNotification(paymentNotification) } else { guard let target = getTarget( recipientAddress: recipientAddress, groupId: groupId, dataMessage: dataMessage, tx: tx, ) else { return nil } guard let messageParams = self.parseMessageParams( sentProto: sentProto, serverTimestamp: serverTimestamp, dataMessage: dataMessage, target: target, tx: tx, ) else { return nil } type = .message(messageParams) } var recipientStates = [SignalServiceAddress: TSOutgoingMessageRecipientState]() for statusProto in sentProto.unidentifiedStatus { guard let serviceId = ServiceId.parseFrom( serviceIdBinary: statusProto.destinationServiceIDBinary, serviceIdString: statusProto.destinationServiceID, ), statusProto.hasUnidentified else { owsFailDebug("Delivery status proto is missing value.") continue } let recipientState = TSOutgoingMessageRecipientState( status: .sent, statusTimestamp: sentProto.timestamp, wasSentByUD: statusProto.unidentified, errorCode: nil, ) recipientStates[SignalServiceAddress(serviceId)] = recipientState } guard validateTimestampsMatch(type: type, sentProto: sentProto, dataMessage: dataMessage) else { return nil } return .init( type: type, timestamp: sentProto.timestamp, recipientStates: recipientStates, ) } private static func validateTimestampsMatch( type: SentMessageTranscriptType, sentProto: SSKProtoSyncMessageSent, dataMessage: SSKProtoDataMessage, ) -> Bool { switch type { case .message, .expirationTimerUpdate, .paymentNotification: // We only validate these types break case .recipientUpdate: // Don't validate these types, as was done historically. return true } guard sentProto.timestamp == dataMessage.timestamp else { owsFailDebug("Transcript timestamps do not match, discarding message.") // This transcript is invalid, discard it. return false } return true } private static func parseMessageParams( sentProto: SSKProtoSyncMessageSent, serverTimestamp: UInt64, dataMessage: SSKProtoDataMessage, target: SentMessageTranscriptTarget, tx: DBWriteTransaction, ) -> SentMessageTranscriptType.Message? { let isViewOnceMessage = dataMessage.hasIsViewOnce && dataMessage.isViewOnce let bodyRanges = dataMessage.bodyRanges.isEmpty ? MessageBodyRanges.empty : MessageBodyRanges(protos: dataMessage.bodyRanges) var body = dataMessage.body.map { DependenciesBridge.shared.attachmentContentValidator.truncatedMessageBodyForInlining( MessageBody(text: $0, ranges: bodyRanges), tx: tx, ) } let validatedContactShare: ValidatedContactShareProto? if let contactShareProto = dataMessage.contact.first { let contactShareManager = DependenciesBridge.shared.contactShareManager validatedContactShare = contactShareManager.validateAndBuild(for: contactShareProto) } else { validatedContactShare = nil } let validatedLinkPreview: ValidatedLinkPreviewProto? if let linkPreviewProto = dataMessage.preview.first { do { let linkPreviewManager = DependenciesBridge.shared.linkPreviewManager validatedLinkPreview = try linkPreviewManager.validateAndBuildLinkPreview( from: linkPreviewProto, dataMessage: dataMessage, ) } catch LinkPreviewError.invalidPreview { // Just drop the link preview, but keep the message Logger.warn("Dropping invalid link preview; keeping message") validatedLinkPreview = nil } catch { owsFailDebug("Unexpected error for incoming synced link preview proto! \(error)") return nil } } else { validatedLinkPreview = nil } let validatedMessageSticker: ValidatedMessageStickerProto? if let stickerProto = dataMessage.sticker { let messageStickerManager = DependenciesBridge.shared.messageStickerManager do { validatedMessageSticker = try messageStickerManager.buildValidatedMessageSticker(from: stickerProto) } catch { owsFailDebug("Unexpected error for incoming message sticker! \(error)") return nil } } else { validatedMessageSticker = nil } let giftBadge = OWSGiftBadge.maybeBuild(from: dataMessage) if giftBadge != nil, target.thread.isGroupThread { owsFailDebug("Ignoring gift sent to group") return nil } let validatedQuotedReply: ValidatedQuotedReply? if let quoteProto = dataMessage.quote { let quotedReplyManager = DependenciesBridge.shared.quotedReplyManager do { validatedQuotedReply = try quotedReplyManager.validateAndBuildQuotedReply( from: quoteProto, threadUniqueId: target.thread.uniqueId, tx: tx, ) } catch { owsFailDebug("Unexpected error for incoming quote reply! \(error)") return nil } } else { validatedQuotedReply = nil } let validatedPollCreate: ValidatedIncomingPollCreate? if let pollCreateProto = dataMessage.pollCreate { let pollMessageManager = DependenciesBridge.shared.pollMessageManager do { validatedPollCreate = try pollMessageManager.validateIncomingPollCreate( pollCreateProto: pollCreateProto, tx: tx, ) } catch { owsFailDebug("Unexpected error for incoming poll create! \(error)") return nil } body = validatedPollCreate!.messageBody } else { validatedPollCreate = nil } let storyTimestamp: UInt64? let storyAuthorAci: Aci? if let storyContext = dataMessage.storyContext, storyContext.hasSentTimestamp, storyContext.hasAuthorAci || storyContext.hasAuthorAciBinary { storyTimestamp = storyContext.sentTimestamp storyAuthorAci = Aci.parseFrom(serviceIdBinary: storyContext.authorAciBinary, serviceIdString: storyContext.authorAci) guard storyAuthorAci != nil else { owsFailDebug("Couldn't parse story author") return nil } } else { storyTimestamp = nil storyAuthorAci = nil } return SentMessageTranscriptType.Message( target: target, body: body, attachmentPointerProtos: dataMessage.attachments, validatedContactShare: validatedContactShare, validatedQuotedReply: validatedQuotedReply, validatedLinkPreview: validatedLinkPreview, validatedMessageSticker: validatedMessageSticker, validatedPollCreate: validatedPollCreate, giftBadge: giftBadge, isViewOnceMessage: isViewOnceMessage, expirationStartedAt: sentProto.expirationStartTimestamp, expirationDurationSeconds: dataMessage.expireTimer, expireTimerVersion: dataMessage.expireTimerVersion, storyTimestamp: storyTimestamp, storyAuthorAci: storyAuthorAci, ) } private static func getTarget( recipientAddress: SignalServiceAddress?, groupId: GroupIdentifier?, dataMessage: SSKProtoDataMessage, tx: DBWriteTransaction, ) -> SentMessageTranscriptTarget? { if let groupId { guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { owsFailDebug("Missing thread for group.") return nil } if let groupContextV2 = dataMessage.groupV2 { guard groupThread.isGroupV2Thread else { owsFailDebug("Invalid thread for v2 group.") return nil } guard groupContextV2.hasRevision else { owsFailDebug("Missing revision.") return nil } let revision = groupContextV2.revision guard let groupModel = groupThread.groupModel as? TSGroupModelV2, revision <= groupModel.revision else { owsFailDebug("Unexpected revision.") return nil } } else { owsFailDebug("Missing group context.") return nil } return .group(groupThread) } else if let recipientAddress { let thread = TSContactThread.getOrCreateThread( withContactAddress: recipientAddress, transaction: tx, ) return .contact( thread, .token( forProtoExpireTimerSeconds: dataMessage.expireTimer, version: dataMessage.expireTimerVersion, ), ) } else { return nil } } private init( type: SentMessageTranscriptType, requiredProtocolVersion: UInt32? = nil, timestamp: UInt64, recipientStates: [SignalServiceAddress: TSOutgoingMessageRecipientState], ) { self.type = type self.requiredProtocolVersion = requiredProtocolVersion self.timestamp = timestamp self.recipientStates = recipientStates } }